# Dracma — guia de integração completo (llms-full) > Arquivo gerado por `scripts/gerar_llms_full.py` a partir de `docs/`. Não edite à mão: edite a página de > origem e gere de novo. Markdown puro, sem HTML. Como ler este arquivo: - Cada página começa com um cabeçalho de nível 1 estável no formato `# Arquivo: `; os títulos de dentro da página foram rebaixados um nível (`#` virou `##`). Links relativos valem a partir do arquivo de origem indicado no cabeçalho. - A primeira página (`docs/referencia-rapida.md`) traz as regras MUST/NEVER do integrador (ids `R01`…), a tabela de endpoints, a de erros e a de estados. Em conflito, vale o contrato normativo (Contrato de integração v1 1.1.0); as diferenças entre o código atual e o contrato estão em `docs/referencia/divergencias.md`. - No fim vêm, sem alteração, os vetores de assinatura normativos e o `openapi.yaml` que a API serve. --- # Arquivo: docs/referencia-rapida.md ## Referência rápida > Para: Dev — Conferido em 2026-10-18. Uma página com as regras obrigatórias do integrador, os endpoints, os erros, os estados e os eventos. É o formato que uma pessoa consulta com o conector aberto e que um agente de código usa como requisito de aceite. Cada linha aponta para a página que explica o assunto por inteiro. Onde o código de hoje difere do contrato, veja [Divergências conhecidas](referencia/divergencias.md). ### O que é Cinco blocos, nesta ordem: [regras](#1-regras-do-integrador-must-never), [endpoints](#2-endpoints), [erros](#3-erros-e-o-que-fazer), [estados](#4-estados-e-efeito-no-livro-do-integrador) e [eventos](#5-eventos-de-webhook). As palavras MUST e NEVER têm o sentido da RFC 2119. Cada regra tem um id estável (`R01` a `R36`) que você pode citar em testes e em revisões. ### Quando usar - Ao revisar o conector antes do go-live: percorra as regras e prove cada uma ([Checklist de go-live](go-live/checklist.md)). - Ao decidir o que fazer com uma resposta ou um estado: use as tabelas 3 e 4. - Ao instruir um agente de código: entregue esta página e o [`llms.txt`](https://docs.dracma.cfd/llms.txt) como requisitos. ### Como funciona #### 1. Regras do integrador (MUST / NEVER) ##### Chamadas ao Dracma - **R01 MUST** chamar o Dracma **só do servidor** do conector. **NEVER** do navegador, do app do jogador ou de código com segredo em bundle (`NEXT_PUBLIC_*`, `VITE_*`). - **R02 MUST** assinar toda requisição: pedido canônico de 6 linhas sem `\n` final, caminho **com** `/v1` e query exatamente como enviada, sha256 do corpo cru, `HMAC-SHA256` com o segredo da chave ([Pedido canônico e assinatura](api/autenticacao.md#pedido-canonico-e-assinatura)). - **R03 MUST** gerar nonce de **32 hex minúsculos por CSPRNG**, novo em **toda** requisição, inclusive em repetição. **NEVER** `Math.random()` nem `math/rand`. - **R04 MUST** mandar `Dracma-Timestamp` em **segundos** e manter o relógio por NTP (desvio acima de 5 s é alerta; a janela é de ±300 s). - **R05 MUST** serializar o corpo **uma vez** e enviar exatamente os bytes assinados. - **R06 MUST** reproduzir os vetores de [`vetores-assinatura.json`](exemplos/README.md) byte a byte no CI, inclusive recusar o negativo. - **R07 MUST** usar timeout de cliente de 10 s ([Timeouts](conceitos/convencoes-http.md#timeouts)). ##### Idempotência e repetição - **R08 MUST** mandar `Idempotency-Key` em **todo** `POST` (inclusive `cancel` e `/sandbox/*`). - **R09 MUST** usar `Idempotency-Key = external_id` em `POST /deposits` e `POST /withdrawals`, e gravar os dois no seu banco **antes** da chamada (outbox). - **R10 NEVER** gerar `Idempotency-Key` nova para repetir um `POST` de dinheiro. Repetição = **mesma** chave, **mesmos** bytes, timestamp, nonce e assinatura novos. - **R11 MUST** repetir `5xx`, `429`, `409 idempotency_in_progress`, timeout e erro de rede com a mesma chave, respeitando `Retry-After` (backoff exponencial com jitter). Veja a [árvore de decisão](api/idempotencia.md#arvore-de-decisao). - **R12 NEVER** repetir um `4xx` (é decisão do Dracma). Tentar de novo depois de uma recusa é uma operação nova, com `external_id` novo. - **R13 MUST** tratar `code` desconhecido pela classe HTTP e pelo `retryable` (4xx não repete; 5xx e 429 repetem). ##### Webhooks e fetch-back - **R14 MUST** verificar a assinatura do webhook sobre o **corpo cru**, antes de interpretar o JSON, aceitando qualquer `v1` com qualquer segredo ativo (lista de 1 ou 2), em tempo constante; janela de 300 s; `Dracma-Timestamp` igual a `t=` ([13 passos](webhooks/receptor.md#os-13-passos-nesta-ordem)). - **R15 MUST** responder `400` se `Dracma-Event-Id` for diferente do `id` do corpo e deduplicar por `Dracma-Event-Id` (guarde por no mínimo 7 dias): duplicata recebe `200` sem efeito. - **R16 MUST** fazer **fetch-back** (`GET /deposits/{id}` ou `GET /withdrawals/{id}` com a própria credencial) **antes** de mover dinheiro. **NEVER** creditar, debitar ou devolver reserva pelo corpo do webhook. - **R17 MUST** conferir no fetch-back: `external_id` é seu; `amount.amount`, `amount.currency` e `player_ref` idênticos ao que você registrou. Divergência: não move dinheiro, abre revisão e alerta. - **R18 MUST** ignorar estado com `version` menor ou igual à última aplicada para o objeto (responda `200`). - **R19 MUST** responder ao webhook em até 10 s; `503` quando o fetch-back falhar por rede ou `5xx` (o Dracma reentrega). - **R20 MUST** rodar um job de `GET /events?after=<último evento aplicado>` **a cada 5 minutos**, com cursor persistido, passando cada evento pelo mesmo caminho do webhook ([Recuperação com `GET /events`](webhooks/recuperacao-get-events.md)). ##### Dinheiro - **R21 MUST** creditar depósito **só** em `paid`, **uma vez** (idempotente por `external_id`), pelo valor que **você** registrou na criação. - **R22 NEVER** mover dinheiro em estado, tipo de evento, `method`, `kind` ou motivo **desconhecido**: trate como não terminal, responda `200` e confira depois. - **R23 MUST** reservar (debitar) o saldo do jogador **antes** de `POST /withdrawals`, na mesma transação que grava o saque local. - **R24 MUST** devolver a reserva **só** em `rejected`, `failed` ou `cancelled` (lidos por fetch-back) ou em `4xx` conclusivo na criação. **NEVER** em timeout, `5xx` ou `429`. - **R25 NEVER** debitar nem recreditar automaticamente em `refunded`, `chargeback`, `returned` ou pagamento depois de `expired`: abra `needs_decision`, dispare alerta e deixe uma pessoa decidir ([Política de exceções](conector/politica-de-excecoes.md)). - **R26 MUST** representar dinheiro como string de inteiro na menor unidade (`"5000"` = R$ 50,00). **NEVER** float ([`Money`](conceitos/convencoes-http.md#dinheiro-money)). ##### Dados do jogador - **R27 MUST** usar `player.external_ref` opaco e imutável. **NEVER** e-mail, telefone ou documento como id. - **R28 MUST** mandar `player.ip` (IP do jogador, não o do seu servidor) em todo saque; sem ele o saque nunca é automático. - **R29 MUST** ler as exigências da operação em `GET /currencies` (`method_details[].deposit_requirements`) e mandar os campos exigidos (por exemplo, `player.name` e `player.email`), vindos do cadastro do jogador. **NEVER** inventados ([Exigências por operação](referencia/depositos.md#exigencias-por-operacao)). - **R30 NEVER** usar, fora de produção, e-mail ou documento de pessoa real (inclusive o seu). E-mail de teste só em `.test`, `.invalid` ou `.example`; CPF de teste com dígito verificador válido ([Dados de teste](sandbox/dados-de-teste.md)). - **R31 NEVER** mostrar ao jogador o motivo de bloqueio ou de revisão (listas, `PAYER_MISMATCH`...). ##### Segredos, rede e operação - **R32 MUST** guardar segredo de chamada e segredo de webhook em cofre ou arquivo `0400` do usuário do serviço. **NEVER** em repositório, imagem, log, ticket ou variável de front ([Segredos e vazamento](seguranca/segredos-e-credenciais.md)). - **R33 MUST** manter a lista de IPs da operação com todos os IPs de saída do conector, mesmo que hoje ela esteja registrada e não aplicada, e apresentar o certificado de cliente se a sua instância ligar o mTLS ([Rede, allowlist e mTLS](seguranca/rede-allowlist-e-mtls.md)). - **R34 MUST** registrar `Dracma-Trace-Id` em todo log de chamada. **NEVER** logar `Dracma-Signature` inteiro, segredo, documento completo ou chave PIX em claro. - **R35 MUST** reconciliar diariamente (`GET /reconciliation`, depois das 02:00 UTC), conferir o digest e a assinatura e tratar divergência como incidente. **NEVER** "consertar" o livro para bater ([Reconciliar com o seu livro](dinheiro/reconciliacao-do-operador.md)). - **R36 MUST** apontar homologação para uma operação Teste (`key_test_`) e produção para uma operação Real (`key_live_`); uma operação Real move dinheiro real em qualquer instância. #### 2. Endpoints Base: `https:///v1` (hosts em [Ambientes, tipos de operação e hosts](conceitos/ambientes-e-hosts.md)). Todos exigem a assinatura da regra R02; todo `POST` exige `Idempotency-Key`. A lista completa de 31 rotas, com o `operationId` de cada uma, está no [Índice de endpoints](referencia/README.md). | Método e caminho | Escopo | Sucesso | Erros típicos | Página | |---|---|---|---|---| | `GET /ping` | qualquer chave | `200 {ok, operation_id, environment, server_time}` | 401, 403 | [Ping](referencia/ping-e-moedas.md#get-v1ping) | | `GET /currencies` | `read` | `200 {data: [Moeda + method_details]}` | 503 | [Moedas](referencia/ping-e-moedas.md#get-v1currencies) | | `POST /deposits` | `deposits:write` | `201` (`pending` ou `failed`) ou `202` (`created`) | 400, 409, 422, 429, 503 | [Criar](referencia/depositos.md#criar-post-v1deposits) | | `GET /deposits` | `read` (listagem 5/s) | `200 {data, next_cursor}` | 400 | [Listar](referencia/depositos.md#listar-get-v1deposits) | | `GET /deposits/{id}` | `read` | `200 Deposito` | 404 | [Consultar](referencia/depositos.md#consultar-get-v1depositsid) | | `POST /deposits/{id}/cancel` | `deposits:write` | `200 Deposito` (`cancelled`) | 404, 409 | [Cancelar](referencia/depositos.md#cancelar-post-v1depositsidcancel) | | `POST /withdrawals` | `withdrawals:write` | `201 Saque` (`approved`, `under_review` ou `requested`) | 400, 409, 422, 503 | [Criar](referencia/saques.md#criar-post-v1withdrawals) | | `GET /withdrawals` | `read` (listagem 5/s) | `200 {data, next_cursor}` | 400 | [Listar](referencia/saques.md#listar-get-v1withdrawals) | | `GET /withdrawals/{id}` | `read` | `200 Saque` | 404 | [Consultar](referencia/saques.md#consultar-get-v1withdrawalsid) | | `POST /withdrawals/{id}/cancel` | `withdrawals:write` | `200 Saque` (`cancelled`) | 404, 409 | [Cancelar](referencia/saques.md#cancelar-post-v1withdrawalsidcancel) | | `GET /players/{external_ref}/limits?currency=` | `read` | `200 LimitesJogador` | 400, 404, 503 | [Limites](referencia/jogadores-e-saldos.md#get-v1playersexternal_reflimits) | | `GET /balances` | `read` | `200 {data: [{available, pending_in, pending_out, as_of}]}` | 503 | [Saldos](referencia/jogadores-e-saldos.md#get-v1balances) | | `GET /events?after=&object_id=&limit=` | `read` (listagem 5/s) | `200 {data: [Evento], next_cursor}` | 400 | [`GET /events`](webhooks/recuperacao-get-events.md) | | `GET /reconciliation?date=¤cy=&cursor=&limit=` | `read` | `200` (a última página traz `totals`, `digest` e o cabeçalho `Dracma-Signature`) | 400, 409 `day_open`, 503 | [Reconciliação](referencia/reconciliacao.md) | | `GET /rules` | `read` | `200` conjunto vigente com `ETag` | 404, 503 | [Ler](referencia/regras.md#ler-get-v1rules) | | `POST /rules/change-requests` | `rules:propose` | `202 {id, status: pending_approval}` | 412 `etag_mismatch` | [Propor](referencia/regras.md#propor-post-v1ruleschange-requests) | | `POST /fx/quotes` | `deposits:write` ou `withdrawals:write` | nenhum | `422 fx_not_enabled` sempre | [Reservadas](referencia/superficies-reservadas.md) | | `GET /fx/quotes/{id}` | `read` | nenhum | `404` sempre | [Reservadas](referencia/superficies-reservadas.md) | | `GET /receipts` | `read` (listagem 5/s) | `200 {data: [], next_cursor: null}` | 400 | [Reservadas](referencia/superficies-reservadas.md) | | `GET /receipts/{id}` | `read` | nenhum | `404` sempre | [Reservadas](referencia/superficies-reservadas.md) | | `POST /withdrawals/{id}/recall` | `withdrawals:write` | nenhum | 400, 404, `409 invalid_state_transition` | [Reservadas](referencia/superficies-reservadas.md) | | `POST /sandbox/deposits/{id}/pay` | `deposits:write` | `200 Deposito` | 400, 404, 409, 503 | [Sandbox](sandbox/README.md#pagar-um-deposito) | | `POST /sandbox/deposits/{id}/{expire,refund,chargeback}` | `deposits:write` | `200 Deposito` | 404, 409 | [Sandbox](sandbox/README.md#rotas-de-controle) | | `POST /sandbox/withdrawals/{id}/{approve,reject,pay,fail,return}` | `withdrawals:write` | `200 Saque` (estado atual) | 400, 404, 409, 503 | [Ações no saque](sandbox/README.md#acoes-no-saque) | | `POST /sandbox/bank-transfers/incoming` | `deposits:write` | nenhum | `400 rail_not_enabled` | [Reservadas](referencia/superficies-reservadas.md) | | `POST /sandbox/outage` | `deposits:write` ou `withdrawals:write` | `200 {down}` | 400 | [Queda simulada](sandbox/README.md#queda-simulada) | | `GET /sandbox/webhooks` | `read` | `200` log de entregas | 503 | [Log de entregas](sandbox/README.md#log-de-entregas-e-reentrega-de-webhook) | | `POST /sandbox/webhooks/redeliver` | `deposits:write` ou `withdrawals:write` | `200 {event_id, scheduled}` | 400, 404, 503 | [Reentrega](sandbox/README.md#log-de-entregas-e-reentrega-de-webhook) | | `GET /openapi.yaml` (fora de `/v1`) | nenhum | `200`, com `ETag` | nenhum | [OpenAPI](referencia/openapi.md) | Em operação Real, todo `/v1/sandbox/*` responde `404`. As rotas de câmbio, recebimentos e `recall` existem só para manter o contrato 1.1.0 inteiro: nenhuma move dinheiro nem cria objeto. #### 3. Erros e o que fazer O catálogo completo (30 `code`), os 22 motivos de validação e as respostas da borda estão em [Erros](api/erros.md#catalogo-de-code-emitidos). | HTTP | `code` | Repetir? | Ação | |---|---|---|---| | 400 | `validation_failed` | não | corrija o pedido (`errors[].field` e `reason`) | | 400 | `currency_not_enabled`, `method_not_enabled`, `rail_not_enabled` | não | configuração da operação; confira `GET /currencies` | | 401 | `invalid_signature`, `timestamp_out_of_window`, `replayed_nonce`, `unknown_key` | não | assinatura, relógio, nonce, chave e host ([Problemas de integração](ajuda/troubleshooting.md)) | | 403 | `ip_not_allowed`, `insufficient_scope`, `operation_suspended` | não | escopo da chave, suporte ([FAQ](ajuda/faq.md)) | | 404 | `not_found` | não | id ou operação errados (objeto de outra operação também é `404`) | | 409 | `idempotency_key_reused` | não | bug: chave reusada com outro corpo | | 409 | `duplicate_external_id` | não | use o objeto em `existing` | | 409 | `invalid_state_transition` | não | consulte o estado atual | | 409 | `idempotency_in_progress` | **sim** | mesma chave depois do `Retry-After` | | 409 | `day_open` | **sim, depois** | agende para depois do `Retry-After`, sem laço | | 412 | `etag_mismatch` | não | releia `GET /rules` e proponha de novo | | 413 | `validation_failed` (`too_large`) | não | corpo de até 64 KiB | | 422 | `amount_below_minimum`, `amount_above_maximum`, `document_invalid`, `document_mismatch`, `player_blocked`, `destination_blocked`, `velocity_exceeded`, `insufficient_operation_balance`, `fx_not_enabled` | não | recusa de negócio, sem objeto; num saque, devolva a reserva | | 429 | `rate_limited` | **sim** | mesma chave depois do `Retry-After` ([Limites](api/limites.md#baldes-de-taxa)) | | 500 | `internal_error` | **sim** | mesma chave; abra chamado com o `trace_id` se persistir | | 503 | `service_unavailable` | **sim** | mesma chave depois do `Retry-After` ([Modo degradado](api/erros.md#modo-degradado)) | #### 4. Estados e efeito no livro do integrador A tradução completa para os estados do seu produto, com mensagens, está em [Estados no seu produto](conector/estados-no-produto.md). ##### Depósito | Estado | Terminal? | Efeito | |---|---|---| | `created` | não | nenhum | | `pending` | não | nenhum (mostre o PIX) | | `under_review` | não | nenhum | | `paid` | sim, para o crédito | **credita uma vez** | | `expired`, `cancelled`, `failed`, `returned` | sim | nenhum (mesmo com `paid_amount` preenchido) | | `refunded`, `chargeback` | sim | débito por política sua: `needs_decision` (só no sandbox hoje) | | `pending_compliance`, `frozen`, `paid_partial` | não alcançáveis em PIX | nenhum automático | | desconhecido | tratar como não terminal | nenhum | ##### Saque (a reserva é feita antes do `POST`) | Estado | Terminal? | Efeito na reserva | |---|---|---| | `requested`, `under_review`, `approved`, `processing` | não | mantém | | `paid` | sim (pode ir a `returned`) | finaliza | | `rejected`, `failed`, `cancelled` | sim | **devolve** | | `returned` | sim | nenhum automático: `needs_decision` | | `pending_compliance`, `sent`, `in_transit`, `recall_requested`, `recalled` | não alcançáveis em PIX | mantém | | desconhecido | tratar como não terminal | mantém | #### 5. Eventos de webhook São **17 tipos** emitidos hoje ([Catálogo de eventos](webhooks/catalogo-de-eventos.md)). | Objeto | Eventos | |---|---| | Depósito (9) | `deposit.pending`, `deposit.under_review`, `deposit.paid`, `deposit.expired`, `deposit.cancelled`, `deposit.failed`, `deposit.returned`, `deposit.refunded`, `deposit.chargeback` | | Saque (8) | `withdrawal.under_review`, `withdrawal.approved`, `withdrawal.processing`, `withdrawal.paid`, `withdrawal.rejected`, `withdrawal.failed`, `withdrawal.cancelled`, `withdrawal.returned` | `deposit.refunded` e `deposit.chargeback` só ocorrem no sandbox hoje. !!! note "Reservado no contrato" Os tipos `deposit.pending_compliance`, `deposit.frozen`, `deposit.paid_partial`, `withdrawal.pending_compliance`, `withdrawal.sent`, `withdrawal.in_transit`, `withdrawal.recall_requested`, `withdrawal.recall_rejected`, `withdrawal.recalled` e `receipt.*` pertencem ao contrato 1.1.0, mas nenhum tem gatilho hoje. Trate qualquer `type` desconhecido pela regra R22. !!! warning "Não disponível" `rules.updated` aparece em listas antigas, mas o Dracma nunca o emite e ele não pode ser assinado. Para saber se as regras mudaram, consulte `GET /v1/rules`. ### Exemplos Um depósito do começo ao fim, em uma linha por passo: grave `external_id` e a intenção (R09); `POST /deposits` assinado com `Idempotency-Key` igual ao `external_id` (R02, R08); receba `deposit.paid`, verifique a assinatura e deduplique (R14, R15); faça o fetch-back e confira valor, moeda e jogador (R16, R17); credite uma vez, na mesma transação que grava a `version` (R18, R21). O código completo está em [Receptor passo a passo](webhooks/receptor.md) e em [Exemplos executáveis](exemplos/README.md). ### Erros e casos de borda - Uma regra que parece conflitar com o seu produto não se negocia pelo código: abra a página da regra, entenda o motivo e, se necessário, fale com o suporte ([Suporte](ajuda/suporte.md)). - Esta página resume. Em divergência com uma página de detalhe, vale a página de detalhe, e a divergência é um erro desta página. ### Veja também - [Início](README.md) - [Glossário](conceitos/glossario.md) - [Divergências conhecidas](referencia/divergencias.md) - [Checklist de go-live](go-live/checklist.md) --- # Arquivo: docs/README.md ## Dracma — guia de integração > Para: Dev · Fin · Adm — Conferido em 2026-10-18. O Dracma é um **gateway de pagamento multi-tenant**. O serviço da sua empresa (o **conector**) chama a API do Dracma para cobrar depósitos PIX e pagar saques PIX dos seus jogadores. O Dracma fala com o PSP, aplica o motor de regras (saque automático ou revisão humana) e avisa cada mudança por **webhook assinado**. As pessoas da sua empresa acompanham e decidem no **painel**. Este guia foi escrito para você integrar sozinho: ler, implementar, provar o conector no sandbox e cumprir o checklist de produção. ### O que é Três grupos de pessoas usam este guia. Escolha a trilha do seu papel. | Trilha | Para quem | O que você entrega | |---|---|---| | [Dev](#trilha-dev) | Quem escreve o conector | Chamadas assinadas, webhook, repetição segura, reconciliação | | [Financeiro](#trilha-financeiro) | Quem responde pelo caixa e pela conferência | Saldo, tarifas, devoluções e conciliação diária | | [Administrador](#trilha-administrador) | Quem cuida da conta, da equipe e do go-live | Acesso, chaves, regras, webhook e checklist de produção | Os termos curtos que aparecem em todo o guia (tenant, operação, conector, jogador, PSP) estão em [Visão geral](conceitos/visao-geral.md). Os demais estão no [Glossário](conceitos/glossario.md). !!! note "Regra de precedência" Este guia segue o Contrato de integração v1, versão 1.1.0, e o [OpenAPI publicado](referencia/openapi.md) que a API serve. O OpenAPI dá a **forma** (campos e tipos); o contrato dá a **semântica** (assinatura, idempotência, máquinas de estado). Se o guia e o contrato divergirem, vale o contrato, e a divergência é um erro do guia. As diferenças conhecidas entre o contrato e a API de hoje estão em [Divergências conhecidas](referencia/divergencias.md). Mudança incompatível só acontece como `/v2`. A v1 só recebe acréscimos (campos, estados, eventos e códigos novos), por isso o seu código deve ser um **leitor tolerante**. Veja [Versionamento](versionamento/politica.md). ### Quando usar - Está começando: leia [Visão geral](conceitos/visao-geral.md) e [Do zero ao go-live](onboarding/README.md). - Já tem a chave de Teste: vá direto ao [Primeiro depósito e primeiro saque](onboarding/quickstart-sandbox.md). - Está com o conector aberto e quer consultar uma regra, um endpoint, um erro ou um estado: use a [Referência rápida](referencia-rapida.md). - Algo falhou: comece por [Problemas de integração](ajuda/troubleshooting.md) ou pelo [FAQ](ajuda/faq.md). ### Como funciona #### Trilha Dev 1. [Visão geral](conceitos/visao-geral.md) e [Ambientes, tipos de operação e hosts](conceitos/ambientes-e-hosts.md): o que é do Dracma, o que é do seu conector, qual host e qual chave usar. 2. [Convenções HTTP](conceitos/convencoes-http.md): `Money` sem float, datas, IDs, timeouts e limites de corpo. 3. [Autenticação HMAC](api/autenticacao.md): pedido canônico, nonce, janela de 300 s e a [ordem de verificação](api/autenticacao.md#ordem-de-verificacao). Reproduza os vetores antes de qualquer outra coisa. 4. [Idempotência e repetição](api/idempotencia.md) e [Erros](api/erros.md): `Idempotency-Key = external_id`, o que repetir e o que não repetir. 5. [Índice de endpoints](referencia/README.md), [Depósitos](referencia/depositos.md) e [Saques](referencia/saques.md): corpos, estados e respostas. 6. [Webhooks](webhooks/README.md), [Receptor passo a passo](webhooks/receptor.md) e [Recuperação com `GET /events`](webhooks/recuperacao-get-events.md): verificar, deduplicar, fazer fetch-back e fechar lacunas. 7. [Estados no seu produto](conector/estados-no-produto.md) e [Arquitetura do conector](conector/arquitetura.md): como traduzir os estados do Dracma para o seu produto. 8. [Sandbox](sandbox/README.md), [Receitas por caminho](sandbox/receitas.md) e [Suíte de conformidade](sandbox/conformidade.md): provar cada caminho antes de produção. 9. [Exemplos executáveis](exemplos/README.md): clientes em Go, Node, PHP e shell, com testes nos vetores. #### Trilha Financeiro 1. [Modelo de dinheiro](dinheiro/README.md): quando o dinheiro se move e o que significa cada saldo. 2. [Saldos e caixa livre](dinheiro/saldos.md) e [Dia, fuso e horários](dinheiro/fuso-dia-e-horarios.md): o que `GET /v1/balances` mostra e como o "dia" é contado. 3. [Ciclo do depósito](dinheiro/ciclo-do-deposito.md) e [Ciclo do saque](dinheiro/ciclo-do-saque.md): cada estado e o efeito no seu livro. 4. [Decisão, limites e regras](dinheiro/decisao-limites-e-regras.md) e [Revisão de depósitos e saques](painel/revisao-de-depositos-e-saques.md): por que um saque vai para revisão e quem decide. 5. [Tarifas](dinheiro/tarifas.md) e [Devoluções, estornos e disputas](dinheiro/devolucoes-estornos-e-disputas.md). 6. [Reconciliar com o seu livro](dinheiro/reconciliacao-do-operador.md) e [Reconciliação no painel](painel/reconciliacao.md): a conferência diária. 7. [Política de exceções de dinheiro](conector/politica-de-excecoes.md) e [Runbook de dinheiro](dinheiro/runbook.md): o que fazer quando algo não bate. #### Trilha Administrador 1. [Do zero ao go-live](onboarding/README.md) e [O que preparar](onboarding/pre-requisitos.md): quem faz o quê, em quanto tempo. 2. [O painel](painel/README.md), [Conta, login e dispositivos](painel/conta-e-login.md) e [Equipe e papéis](painel/equipe-e-papeis.md): acesso e aprovações de outra pessoa. 3. [Tenants e operações](painel/tenants-e-operacoes.md), [Chaves de API](painel/chaves-de-api.md) e [Webhooks no painel](painel/webhooks.md). 4. [Regras, listas e emergência](painel/regras-e-listas.md), [Alertas e e-mails](painel/alertas.md) e [Auditoria](painel/auditoria.md). 5. [De Teste para Real](onboarding/promocao-para-producao.md), [Checklist de go-live](go-live/checklist.md) e [Dia do corte e rollback](go-live/dia-do-corte-e-rollback.md). 6. [Segurança](seguranca/rede-allowlist-e-mtls.md): rede, [segredos](seguranca/segredos-e-credenciais.md) e [dados pessoais](seguranca/dados-e-privacidade.md). #### O que está disponível hoje Conferido contra o código em **2026-10-18** (API `17908a2`, webhooks `332d6cb`, ledger `6679cce`, regras `3a0f8a3`). | Item | Estado hoje | |---|---| | Método `pix` em BRL, depósito e saque | **Disponível** | | Assinatura HMAC, escopos, idempotência, limites de taxa, erros `problem+json`, `GET /v1/ping` | **Disponível** | | `POST`/`GET /v1/deposits`, `GET /v1/deposits/{id}`, `POST /v1/deposits/{id}/cancel` | **Disponível** ([Depósitos](referencia/depositos.md)) | | `POST`/`GET /v1/withdrawals`, `GET /v1/withdrawals/{id}`, `POST /v1/withdrawals/{id}/cancel` | **Disponível** ([Saques](referencia/saques.md)) | | `GET /v1/events`, `/reconciliation`, `/balances`, `/currencies`, `/rules`, `/players/{ref}/limits` e `POST /v1/rules/change-requests` | **Disponível** ([Índice de endpoints](referencia/README.md)) | | Webhooks assinados: 17 tipos de evento, 9 de depósito e 8 de saque | **Disponível** ([Catálogo de eventos](webhooks/catalogo-de-eventos.md)) | | Rotas `/v1/sandbox/*` | **Só em operação Teste**; em operação Real respondem `404` | | `deposit.refunded` e `deposit.chargeback` | **Só no sandbox** hoje: nenhum caminho de produção leva um depósito a esses estados | | `deposit.returned` e `withdrawal.returned` | **Existem em produção** (devolução na revisão e devolução informada pelo banco) | | Câmbio, recebimentos e `recall` de saque | **Reservado no contrato**: as rotas respondem de forma previsível, sem efeito ([Rotas e campos reservados](referencia/superficies-reservadas.md)) | !!! warning "Não disponível" Cartão, cripto, boleto, transferência bancária, wire e SWIFT. Pedir um desses métodos devolve `400 method_not_enabled`. Use `GET /v1/currencies` para saber o que a sua operação aceita. !!! warning "Não disponível" O Dracma não oferece: tarifas descontadas do crédito (o crédito é o valor cheio; a tarifa é por contrato e sem extrato), aporte de caixa (o caixa só cresce por depósito pago ou devolução), página de status pública, lista de IPs de saída dos webhooks e aplicação da lista de IPs às chamadas (a lista é registrada, mas hoje não é aplicada). ### Exemplos O caminho mais curto até a primeira chamada que funciona: 1. Peça ao Dracma o tenant e a operação Teste ([Do zero ao go-live](onboarding/README.md)). 2. Crie a chave `key_test_` no painel ([Chaves de API](painel/chaves-de-api.md)). 3. Reproduza os vetores de assinatura com um dos [exemplos](exemplos/README.md) e chame `GET /v1/ping`. 4. Siga o [Primeiro depósito e primeiro saque](onboarding/quickstart-sandbox.md). ### Erros e casos de borda - **Operação Real move dinheiro real em qualquer instância**, inclusive no hml. Só a operação Teste é isolada do PSP. Veja [Ambientes, tipos de operação e hosts](conceitos/ambientes-e-hosts.md). - **O webhook só avisa.** O dinheiro se move depois do fetch-back, nunca pelo corpo do webhook. - **Estado, evento ou código desconhecido** nunca move dinheiro. Trate como não terminal e confirme depois. - **Fonte de verdade.** Em dúvida sobre o que a API faz hoje, rode no sandbox. Em dúvida sobre o contrato, vale o contrato. ### Veja também - [Referência rápida](referencia-rapida.md): as regras `R01` a `R36`, os endpoints, os erros e os estados em uma página. - [Glossário](conceitos/glossario.md) - [Ajuda](ajuda/README.md): FAQ, problemas de integração e suporte. - [Changelog](versionamento/changelog.md) - [Casos](casos/README.md): um operador real, como exemplo de decisões de integração. - Para agentes de código: [`llms.txt`](https://docs.dracma.cfd/llms.txt) e [`llms-full.txt`](https://docs.dracma.cfd/llms-full.txt). --- # Arquivo: docs/conceitos/visao-geral.md ## Visão geral > Para: Dev · Fin · Adm — Conferido em 2026-10-18. O Dracma recebe depósitos PIX do seu produto, paga saques PIX dos seus jogadores e avisa cada mudança por webhook. Esta página define os termos que o resto do guia usa e mostra o caminho de ponta a ponta de um depósito e de um saque. ### O que é Seis termos aparecem em todas as páginas. | Termo | O que é | |---|---| | **Tenant** | A sua empresa (pessoa jurídica) no Dracma. Agrupa pessoas, operações e a trilha de auditoria. O Dracma cria o tenant; você não se cadastra sozinho. | | **Operação** | Uma marca em um país e em um ambiente, por exemplo `minhamarca-br-hml` e `minhamarca-br-prd`. É a unidade de **credencial, regra, moeda, webhook e saldo**. Tudo o que você faz na API pertence a uma operação. | | **Tipo da operação** | **Teste** (simulador, sem dinheiro real, chave `key_test_`) ou **Real** (PSP de verdade, dinheiro real, chave `key_live_`). Veja [Ambientes, tipos de operação e hosts](ambientes-e-hosts.md#matriz-instancia-tipo). | | **Conector** | O serviço do seu lado que fala com o Dracma. Só ele guarda a chave e o segredo, e só ele chama a API. | | **Jogador** | O seu usuário final. No Dracma é identificado por `player.external_ref`: um texto opaco e imutável que **nunca** é e-mail, telefone ou CPF. | | **PSP** | O provedor de pagamento que movimenta o PIX de fato. O Dracma escolhe o PSP de cada operação; o seu conector não conversa com ele. | Duas pessoas também têm nome: - **Staff do Dracma:** quem cria tenants, cadastra e ativa gateways e aprova pedidos de operação Real. Veja [Equipe e papéis](../painel/equipe-e-papeis.md). - **Administrador do tenant:** a primeira pessoa da sua empresa no painel. Ela entra por convite, vira dona do tenant e convida o resto da equipe. Tenant e operação **vêm só da sua credencial** (chave de API e assinatura). O conector nunca informa `operation_id` para dizer onde está. ### Quando usar Leia esta página antes de qualquer outra. Quem integra a API segue para [Do zero ao go-live](../onboarding/README.md). Quem opera o dinheiro segue para [Modelo de dinheiro](../dinheiro/README.md). Quem administra a equipe segue para [Conta, login e dispositivos](../painel/conta-e-login.md). ### Como funciona #### O que é do Dracma e o que é do seu conector | Responsabilidade | Dracma | Seu conector | |---|---|---| | Cobrar o PIX no PSP e confirmar o pagamento | sim | não | | Pagar o saque no PSP, com decisão automática, revisão ou bloqueio | sim | não | | Guardar o saldo do **jogador** | não | **sim** (reserva antes do saque) | | Guardar o livro do **operador** (depósitos pagos, saques, devoluções) | sim | confere por reconciliação | | Avisar mudanças de estado | webhook e `GET /events` | recebe, deduplica e confere | | Confirmar o estado antes de mover dinheiro | expõe `GET /deposits/{id}` e `GET /withdrawals/{id}` | **sempre** consulta (fetch-back) | | Repetir uma chamada que falhou | aceita `Idempotency-Key` | repete com a mesma chave | | Bônus, promoções e carteira do jogador | fora da API | seu produto | | Aprovar saque em revisão, mudar regras, gerir equipe | painel | seu time usa o painel | O seu produto sobe e funciona sem o Dracma. Se o Dracma ficar indisponível, o método PIX fica em fila ou desligado, e o resto da sua operação continua. #### Ponta a ponta ```mermaid sequenceDiagram participant J as Jogador participant C as Seu conector participant D as Dracma (API /v1) participant P as PSP J->>C: quer depositar R$ 50,00 C->>C: grava a intenção (external_id) no seu banco C->>D: POST /v1/deposits (assinado, Idempotency-Key) D-->>C: 201 com instruções PIX C-->>J: mostra o PIX copia e cola J->>P: paga o PIX P->>D: confirma o pagamento (callback) D-->>C: webhook deposit.paid C->>D: GET /v1/deposits/{id} (fetch-back) D-->>C: status paid, valor registrado C->>C: credita o jogador, uma vez, por external_id ``` ```mermaid sequenceDiagram participant J as Jogador participant C as Seu conector participant D as Dracma (API /v1) participant R as Revisor (painel) participant P as PSP J->>C: pede saque C->>C: reserva o saldo do jogador e grava o external_id C->>D: POST /v1/withdrawals D-->>C: 201 approved (automático) ou under_review opt under_review R->>D: aprova ou recusa no painel end D->>P: envia o PIX P-->>D: confirma D-->>C: webhook withdrawal.paid C->>D: GET /v1/withdrawals/{id} C->>C: finaliza a reserva (ou devolve em rejected, failed, cancelled) ``` #### Várias operações por tenant Um tenant pode ter quantas operações precisar: uma por marca, país e ambiente. Cada operação tem saldo, regras, chaves e webhook **próprios**, e o painel isola uma da outra: quem tem acesso à operação A não vê a B. Uma chave pertence a uma única operação. ### Exemplos Um tenant com duas marcas no Brasil, com Teste e Real separados: | Operação | Tipo | Chave | Webhook | |---|---|---|---| | `marca-a-br-hml` | Teste | `key_test_…` | `https://pagamentos.marca-a.example/dracma` | | `marca-a-br-prd` | Real | `key_live_…` | `https://pagamentos.marca-a.example/dracma` | | `marca-b-br-hml` | Teste | `key_test_…` | `https://pagamentos.marca-b.example/dracma` | O padrão `marca-país-ambiente` é só convenção. O nome da operação aceita letras minúsculas, dígitos e hífen. ### Erros e casos de borda - Chave de Teste em um host de Real (ou o inverso) recebe `401 unknown_key`. É a trava que impede apontar o ambiente errado. - Mandar `operation_id` em um corpo de criação não escolhe a operação: o que vale é a chave. - Em operação Teste nenhum dinheiro se move. Em operação Real o dinheiro é real, em qualquer instância. Veja o aviso em [Ambientes, tipos de operação e hosts](ambientes-e-hosts.md). ### Veja também - [Ambientes, tipos de operação e hosts](ambientes-e-hosts.md) - [Do zero ao go-live](../onboarding/README.md) - [Convenções HTTP](convencoes-http.md) - [Glossário](glossario.md) --- # Arquivo: docs/conceitos/ambientes-e-hosts.md ## Ambientes, tipos de operação e hosts > Para: Dev · Adm — Conferido em 2026-10-18. Duas coisas independentes definem onde uma chamada cai: o **ambiente** (a instância do Dracma, `hml` ou `prd`) e o **tipo da operação** (Teste ou Real). Esta página mostra a combinação de cada um, os hosts, o que cada host publica e como você descobre onde a sua chave está. ### O que é - **Ambiente** é a instância do Dracma que atende o host. Existem duas: **hml** (homologação, no domínio `dracma.cfd`) e **prd** (produção, em um domínio que o Dracma informa no onboarding). - **Tipo da operação** é uma propriedade da operação, escolhida ao criá-la: | Tipo no painel | `environment` na API | Chave | PSP | Dinheiro | |---|---|---|---|---| | **Teste** | `sandbox` | `key_test_…` | simulador do Dracma | **nunca** se move; o PIX é sintético | | **Real** | `production` | `key_live_…` | PSP de verdade | **real**, em qualquer instância | !!! danger "Dinheiro real" Uma operação **Real** move dinheiro de verdade também no hml. Quem testa paga o PIX e recebe o saque de fato. Só uma operação **Teste** é isolada do PSP real. O selo `HML · sandbox` do painel descreve a instância, não o tipo da operação: ele também aparece quando você olha uma operação Real do hml. ### Quando usar - Desenvolva e valide o conector em **hml com uma operação Teste**: nenhum dinheiro, simulador completo, rotas `/v1/sandbox/*` abertas. - Vá para **prd com uma operação Real** quando o [checklist de go-live](../go-live/checklist.md) estiver completo. - Use uma operação **Real no hml** só quando o Dracma tiver habilitado essa via para o seu tenant e você quiser exercitar o PSP de verdade antes da produção. Isso exige as mesmas cautelas de produção. O fluxo do seu lado é: Teste no hml, Real (opcional) no hml, Real em prd. Cada passo pede um pedido e uma chave próprios. Veja [De Teste para Real](../onboarding/promocao-para-producao.md). ### Como funciona #### Hosts Cada papel tem um host. No hml eles são fixos; em prd o Dracma informa o domínio-base no onboarding e os nomes seguem o mesmo padrão (`api.`, `auth.`, `painel.` sob o mesmo domínio-base). | Papel | Host no hml | Em prd | Quem usa | O que atende | |---|---|---|---|---| | API de operações **Teste** | `api.dracma.cfd` | não existe | seu conector | `/v1/*` com chaves `key_test_`; `/v1/sandbox/*` aberto | | API de operações **Real** | `api-live.dracma.cfd` | `api.` do domínio de produção | seu conector | `/v1/*` com chaves `key_live_`; `/v1/sandbox/*` sempre `404` | | Login | `auth.dracma.cfd` | `auth.` do domínio de produção | pessoas | cadastro, login, conta, convite | | Painel | `painel.dracma.cfd` | `painel.` do domínio de produção | pessoas | painel do operador | | Callback dos PSPs | `cb.dracma.cfd` | domínio de callback de produção | os PSPs | recebe a confirmação de pagamento. **Não é seu**: não chame nem configure | | Este guia | `docs.dracma.cfd` | — | todos | documentação estática | Não existe host `sandbox.`. Os exemplos do guia usam `$DRACMA_BASE_URL` para você colocar o host certo. #### Matriz instância × tipo | Instância | Tipo | Host da API | Prefixo da chave | `/v1/sandbox/*` | PSP | Dinheiro real | |---|---|---|---|---|---|---| | hml | Teste | `api.` | `key_test_` | aberto | simulador | não | | hml | Real | `api-live.` | `key_live_` | `404` | PSP real | **sim** | | prd | Teste | não existe | — | — | — | — | | prd | Real | `api.` do domínio de produção | `key_live_` | `404` | PSP real | **sim** | !!! warning "Não disponível" O Dracma não cria operação **Teste** em prd. O pedido volta `422` com `environment_not_allowed`. O Teste do integrador vive no hml. Em prd só existe operação Real. #### O que cada host publica A API pública é só `/v1/*`. Qualquer outro caminho nos hosts da API recebe `404` sem corpo. O mesmo vale para `/v1/sandbox/*` nos hosts de operação Real. O guia e o contrato não descrevem outras rotas porque elas não são publicadas. - O corpo de um pedido tem teto de 1 MB na borda (a API aplica um teto menor; veja [Convenções HTTP](convencoes-http.md#tamanhos-de-corpo)). - O host de login atende as páginas de cadastro, login, conta e convite e os endereços OIDC que o painel usa. A API de serviço do login nunca é pública. - O painel e o login só falam HTTPS. #### Webhooks fora de produção Em hml e em qualquer ambiente que não seja produção, o host da URL do seu webhook precisa ser de um domínio reservado (`.test`, `.invalid`, `.example`) ou de um domínio que o Dracma tenha habilitado para você. Isso vale também para a operação Real do hml. Peça no onboarding a liberação do **domínio do seu conector de homologação**. Túneis públicos temporários (ngrok e semelhantes) não funcionam. Em produção a regra é outra: veja [Seu endpoint](../webhooks/endpoint-e-politica-de-url.md#ambiente-da-url). ### Exemplos #### Descobrir o ambiente: `GET /v1/ping` `GET /v1/ping` aceita qualquer chave válida e devolve o ambiente e a operação **derivados da chave**: ```json { "ok": true, "operation_id": "0192a3b4-0000-7000-8000-0000000000a1", "environment": "sandbox", "server_time": "2026-10-18T12:00:00Z" } ``` `environment` é `sandbox` para operação Teste e `production` para Real. Compare `server_time` com o seu relógio: um desvio acima de 5 s derruba a assinatura (janela de 300 s) muito antes de chegar a esse ponto. Três sinais independentes dizem onde você está: 1. **Prefixo da chave:** `key_test_` é Teste; `key_live_` é Real. 2. **`environment` no ping:** `sandbox` ou `production`. 3. **Selo do painel:** `HML · sandbox` ou `PRD · produção`, no topo. A lista de operações mostra o tipo (Teste ou Real) de cada uma. ```mermaid flowchart TD A["Chamei a API com uma chave"] --> B{"Resposta"} B -- "401 unknown_key" --> C["Chave de um ambiente em outro host.
key_test_ só em api.dracma.cfd,
key_live_ só no host de operação Real"] B -- "200 no ping" --> D{"environment"} D -- "sandbox" --> E["Operação Teste: nenhum dinheiro se move"] D -- "production" --> F["Operação Real: dinheiro real"] B -- "404 em /v1/sandbox/*" --> G["Host de operação Real ou prd:
sandbox nunca existe aí"] ``` ### Erros e casos de borda | O que você vê | Causa | O que fazer | |---|---|---| | `401 unknown_key` | A chave pertence ao outro tipo ou ambiente (por exemplo `key_test_` no host de operação Real), tem formato inválido ou foi revogada | Confira o host contra a [matriz](#como-funciona). Use `GET /v1/ping` com a chave correta | | `404` sem corpo em `/v1/sandbox/...` | O host é de operação Real ou de prd | Rodar rotas de simulação só no host de Teste | | `404` sem corpo em outro caminho | Só `/v1/*` é publicado | Corrija o caminho | | `422 environment_not_allowed` ao criar operação | Ambiente não permitido na instância: prd não cria Teste | Crie o Teste no hml | | Webhook não entrega no hml | Host fora dos reservados e não habilitado | Peça a liberação do domínio no onboarding | Em produção, um `403` sem corpo problem+json vindo da borda (antes da API) indicaria uma restrição de origem aplicada à instância. Veja [Rede, allowlist e mTLS](../seguranca/rede-allowlist-e-mtls.md#lista-de-ips-de-origem). !!! warning "Não disponível" Uma única operação não troca de tipo: não existe converter Teste em Real. A operação Real é outra operação, com outra chave. Veja [De Teste para Real](../onboarding/promocao-para-producao.md). ### Veja também - [Visão geral](visao-geral.md) - [Convenções HTTP](convencoes-http.md) - [Sandbox](../sandbox/README.md) - [Rede, allowlist e mTLS](../seguranca/rede-allowlist-e-mtls.md) - [Do zero ao go-live](../onboarding/README.md) --- # Arquivo: docs/conceitos/convencoes-http.md ## Convenções HTTP > Para: Dev — Conferido em 2026-10-18. Regras que valem para toda chamada à API `/v1`: como representar dinheiro e datas, quais IDs existem de verdade, o que mandar nos cabeçalhos, quanto tempo esperar uma resposta e como o Dracma evolui sem quebrar o seu código. ### O que é A API do Dracma é uma API JSON sobre HTTPS, feita para ser chamada do **servidor** do seu conector, nunca de um navegador. Esta página reúne as convenções comuns. Autenticação, erros, idempotência, paginação e limites têm página própria; os links estão no fim da página. | Item | Convenção | |---|---| | Base | `https:///v1`. O host muda por ambiente: veja [Ambientes, tipos de operação e hosts](ambientes-e-hosts.md#hosts). | | Corpo | JSON em UTF-8, um objeto na raiz. Só `POST` tem corpo. | | Resposta de sucesso | `application/json`. | | Resposta de erro | `application/problem+json` ([Erros](../api/erros.md)). | | Navegador | Não suportado. A API não responde a CORS para o seu domínio; chame do servidor. | | Métodos | `GET`, `HEAD` e `POST`. Qualquer outro método autenticado recebe `403 insufficient_scope`. | ### Quando usar Consulte esta página ao montar o cliente HTTP do conector e ao revisar um erro `400` que você não entende. Ela responde a perguntas como "mando `amount` como número?", "o `created_at` tem fração de segundo?" e "quanto espero antes de desistir de um `POST`?". ### Como funciona #### Dinheiro: `Money` Todo valor monetário é um objeto `Money` com dois campos: - `amount`: **string** com um inteiro na menor unidade da moeda (centavo no BRL). Nunca número JSON, nunca decimal, nunca com sinal ou expoente. - `currency`: código da moeda, por exemplo `BRL`. O `amount` casa com `^(0|[1-9][0-9]{0,37})$`: de 1 a 38 dígitos, **sem zeros à esquerda**. Por isso `"05000"`, `"50.00"`, `"-1"`, `"5e3"` e `5000` (número) são recusados com `400 validation_failed`. Em criação de depósito e de saque o valor também precisa ser maior que zero. ```json { "amount": "5000", "currency": "BRL" } ``` Esse exemplo é R$ 50,00. O número de casas da moeda vem de `exponent` em `GET /v1/currencies`: o BRL tem `exponent` 2. A API nunca devolve nem aceita valor decimal; a conversão é sua. **Converta sem `float`.** Um `float` de 64 bits perde precisão (`0.1 + 0.2`) e nenhum banco do mundo aceita "quase R$ 50,00". Faça a conversão por texto, validando as casas decimais: ```python import re def para_menor_unidade(texto: str, expoente: int) -> str: """'50.00' (BRL, expoente 2) -> '5000'. Sem float.""" m = re.fullmatch(r"(\d+)(?:\.(\d*))?", texto) if not m or len(m.group(2) or "") > expoente: raise ValueError("valor inválido ou com mais casas que a moeda") inteiro, frac = m.group(1), (m.group(2) or "").ljust(expoente, "0") return (inteiro + frac).lstrip("0") or "0" def para_decimal(minima: str, expoente: int) -> str: """'5000' -> '50.00'. Só para exibir ao jogador.""" if not re.fullmatch(r"0|[1-9][0-9]{0,37}", minima): raise ValueError("amount fora do formato do Dracma") s = minima.rjust(expoente + 1, "0") return f"{s[:-expoente]}.{s[-expoente:]}" if expoente else s ``` ```typescript export function paraMenorUnidade(texto: string, expoente: number): string { const m = /^(\d+)(?:\.(\d*))?$/.exec(texto); if (!m || (m[2] ?? "").length > expoente) throw new Error("valor inválido ou com mais casas que a moeda"); const bruto = m[1] + (m[2] ?? "").padEnd(expoente, "0"); return bruto.replace(/^0+(?=\d)/, ""); } ``` | Entrada | Expoente | Saída | |---|---|---| | `"50.00"` | 2 | `"5000"` | | `"0.05"` | 2 | `"5"` | | `"19"` | 2 | `"1900"` | | `"1234.5"` | 2 | `"123450"` | | `"1.234"` | 2 | erro: mais casas do que a moeda | Guarde e some valores como inteiros (`bigint`, `BigInteger`, `numeric(38,0)` no banco). Um `amount` de 38 dígitos não cabe em `int64` nem em `double`. #### Datas e horas Todo instante é **RFC 3339 em UTC**, sempre terminado em `Z`. O que muda é a precisão: | Objeto | Formato dos instantes | Exemplo | |---|---|---| | Depósito (`created_at`, `updated_at`, `paid_at`, `expires_at`), evento, `server_time` do ping | segundos | `2026-10-18T15:04:05Z` | | Saque (`created_at`, `updated_at`, `paid_at`) e `return.returned_at` | segundos **ou** fração de segundo | `2026-10-18T15:04:05.123456Z` | Escreva um *parser* tolerante: aceite com e sem fração (`time.RFC3339Nano` em Go, `Date.parse` em JavaScript, `datetime.fromisoformat` em Python 3.11+). Não compare instantes como texto e não assuma número fixo de casas. Os parâmetros de entrada `updated_since` aceitam as duas formas. Dias de reconciliação e janelas de regra seguem outro calendário (o fuso da operação). Veja [Dia, fuso e horários](../dinheiro/fuso-dia-e-horarios.md). #### Identificadores Os IDs que o Dracma cria são **opacos, com prefixo**. Trate-os como texto: não derive nada do formato, não ordene e não extraia partes. | Prefixo | Objeto | Observação | |---|---|---| | `dep_` | Depósito | Vem em `id` do depósito. | | `wdr_` | Saque | Vem em `id` do saque. | | `evt_` | Evento (webhook e `GET /events`) | É o `Dracma-Event-Id` do webhook. | | `chg_` | Pedido de mudança de regras | Resposta de `POST /rules/change-requests`. | | `key_test_`, `key_live_` | Chave de API (`Dracma-Key-Id`) | O prefixo indica o tipo da operação ([Autenticação](../api/autenticacao.md)). | !!! note "Reservado no contrato" O contrato cita também os prefixos `rcn_` (reconciliação), `rcv_` (recebimento bancário) e `fxq_` (cotação de câmbio). Nenhuma rota os devolve hoje: reconciliação não tem ID próprio, e recebimentos e cotações nunca existem (`GET /receipts/{id}` e `GET /fx/quotes/{id}` respondem sempre `404 not_found`). Veja [Rotas e campos reservados](../referencia/superficies-reservadas.md#recebimentos-bancarios-get-v1receipts-e-get-v1receiptsid). IDs **seus** têm formato controlado por você, dentro de um limite: | Campo | Formato | |---|---| | `external_id` (depósito, saque) | 1 a 64 caracteres `[A-Za-z0-9_.:-]`; único por operação e por tipo | | `player.external_ref` | 1 a 64 caracteres `[A-Za-z0-9_.:-]`; opaco, imutável, **nunca e-mail nem telefone** | | `Idempotency-Key` | 16 a 128 caracteres `[A-Za-z0-9_.:-]` ([Idempotência](../api/idempotencia.md#a-chave)) | O `operation_id` que aparece no ping e nos eventos identifica a sua operação. Ele vem sempre da chave de API; um `operation_id` na raiz do corpo de uma requisição é ignorado, e aninhado em qualquer outro lugar vira `400 validation_failed` (`unknown_field`). #### Cabeçalhos e corpo **Requisição.** | Cabeçalho | Quando | Observação | |---|---|---| | `Dracma-Key-Id`, `Dracma-Timestamp`, `Dracma-Nonce`, `Dracma-Signature` | toda chamada | [Autenticação HMAC](../api/autenticacao.md#os-quatro-cabecalhos). | | `Idempotency-Key` | todo `POST` | Obrigatório, inclusive em `cancel` e em `/sandbox/*`. | | `Content-Type: application/json; charset=utf-8` | `POST` com corpo | **Recomendado**. A API não valida este cabeçalho (interpreta o corpo como JSON de qualquer jeito), mas proxies e bibliotecas HTTP do seu lado dependem dele. | | `Accept` | opcional | Ignorado: a resposta é sempre JSON. | | `If-Match` | `POST /rules/change-requests` | Obrigatório nessa rota. | | `traceparent` | opcional | Se vier, o Dracma continua o seu *trace* (seção Rastreamento). | `POST` de ação sem corpo (`cancel`, por exemplo) leva `Idempotency-Key` e assina o `sha256` de um corpo vazio. O `Content-Type` não é necessário. `HEAD` funciona em toda rota `GET` e devolve só os cabeçalhos; assine com o método `HEAD`. `OPTIONS` e os métodos `PUT`, `PATCH` e `DELETE` não existem e voltam `403 insufficient_scope`. **Validação estrita.** O corpo é recusado com `400 validation_failed` antes de qualquer efeito se tiver: - UTF-8 inválido (`invalid_utf8`); - JSON malformado (`syntax`) ou conteúdo depois do objeto (`trailing_data`); - a mesma chave duas vezes, em qualquer nível, mesmo variando maiúsculas e minúsculas (`duplicate_key`); - campo que o schema não tem (`unknown_field`), inclusive `status`, `version`, `paid_amount` e `decision`; - tipo errado (`type`), por exemplo número onde vai string. A tabela de `reason` está em [Erros](../api/erros.md). #### Tamanhos de corpo Há dois limites em camadas: | Camada | Limite | O que você recebe ao passar | |---|---|---| | API | **64 KiB** | `413` com corpo `problem+json`: `code: validation_failed` e `errors[0] = {field: "body", reason: "too_large"}` | | Entrada pública (borda) | **1 MB** | `413` sem corpo `problem+json` | Um pedido de 100 KiB chega à API e recebe o erro completo. Um pedido de 2 MB é cortado antes, e o seu cliente deve tratar `413` sem corpo. Na prática nenhum corpo legítimo passa de poucos KiB; se o seu passa, o `metadata` está grande demais. #### Timeouts Configure o cliente HTTP do conector com **10 s** de *timeout* total. Dentro disso: | Chamada | Tempo de resposta | Resposta | |---|---|---| | `POST /v1/deposits` | até cerca de 7 s | `201` com as instruções de pagamento prontas, ou `202` com o depósito em `created` se o PSP não respondeu a tempo | | `POST /v1/withdrawals` | rápido; o envio ao banco é assíncrono | **sempre `201`**, nunca `202` | | `POST /v1/sandbox/deposits/{id}/pay` | espera até 3 s pelo evento de pagamento | `200` com o depósito no estado atual | | Demais rotas | normalmente abaixo de 1 s | | Um `202` em depósito não é erro: o objeto existe. Siga pelo webhook e pelo `GET /v1/deposits/{id}`. Um *timeout* do seu lado **não** diz se houve efeito: repita com a mesma `Idempotency-Key` ([Idempotência e repetição](../api/idempotencia.md)). #### Rastreamento Toda resposta `/v1` (sucesso ou erro, inclusive `401`) traz: - `Dracma-Trace-Id`: o ID do *trace* da requisição, 32 caracteres hexadecimais. É o mesmo valor do campo `trace_id` do erro; - `traceparent`: o contexto W3C do *span* do servidor. Se você mandar um `traceparent` válido, o Dracma continua o seu *trace* e o `Dracma-Trace-Id` devolvido é o seu. Guarde o `Dracma-Trace-Id` de **toda** resposta que não seja `2xx` no log do conector. Ao abrir um chamado ([Suporte](../ajuda/suporte.md)), informe esse ID, a hora em UTC e o `operation_id`; com ele o suporte acha a requisição inteira. #### Leitor tolerante e evolução A v1 só cresce: ganha campos, estados, eventos, métodos e códigos de erro. Por isso o seu código deve ser um **leitor tolerante**: - ignore campo desconhecido em qualquer objeto; - trate `status`, `method`, `kind`, `rail` e `type` de evento desconhecidos como **não terminais**: grave, não mova dinheiro e confira depois pelo fetch-back (`GET /deposits/{id}` ou `GET /withdrawals/{id}`); - trate `code` e `errors[].reason` desconhecidos pela classe HTTP e pelo campo `retryable`; - aceite `null` em campo que hoje vem preenchido e vice-versa. A política completa está em [Versionamento](../versionamento/politica.md). ### Exemplos Chamada de ping com o cabeçalho de rastreamento da resposta (`GET /v1/ping`, valores fictícios): ```text HTTP/1.1 200 OK Content-Type: application/json Dracma-Trace-Id: 4bf92f3577b34da6a3ce929d0e0e4736 traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01 {"ok":true,"operation_id":"0192a3b4-5c6d-7e8f-9a0b-1c2d3e4f5a6b","environment":"sandbox","server_time":"2026-10-18T15:04:05Z"} ``` Comparar o `server_time` com o relógio do conector é a forma mais barata de detectar *clock skew* ([Autenticação HMAC](../api/autenticacao.md#janela-de-tempo-nonce-e-anti-replay)). ### Erros e casos de borda | Situação | Resposta | |---|---| | `amount` como número, com sinal, expoente, zeros à esquerda ou mais de 38 dígitos | `400 validation_failed` | | `amount` igual a `"0"` em criação de depósito ou saque | `400 validation_failed` com `reason: required` | | `GET` em rota que só existe como `POST` (por exemplo `GET /v1/deposits/{id}/cancel`) | `404 not_found` | | `POST` (ou `PUT`, `PATCH`, `DELETE`, `OPTIONS`) em rota que não existe ou não aceita esse método | `403 insufficient_scope` (o escopo é conferido antes da rota) | | Rota desconhecida em `GET`, com credencial válida | `404 not_found` | | Rota desconhecida com assinatura inválida | `401`, não `404`: a autenticação vem antes do roteamento | | Barra final (`/v1/deposits/`) | A API ignora a barra, mas o caminho assinado é o que você enviou, com a barra | | Parâmetro de query inválido (`limit=abc`) | `400 validation_failed` | | Caminho fora de `/v1/*` (`/health`, `/`, `/metrics`) | `404` sem corpo, da borda | | Qualquer erro interno | `500 internal_error`, sem detalhe técnico; o `trace_id` leva ao log do Dracma | Falhas de TLS e erros de proxy no caminho até o Dracma não têm corpo `problem+json`: trate resposta que não é JSON como erro de rede e repita com a mesma `Idempotency-Key`. ### Veja também - [Autenticação HMAC](../api/autenticacao.md) - [Erros](../api/erros.md) - [Idempotência e repetição](../api/idempotencia.md) - [Paginação e filtros](../api/paginacao.md) - [Limites e quotas](../api/limites.md) - [Versionamento](../versionamento/politica.md) - [Ambientes, tipos de operação e hosts](ambientes-e-hosts.md) - [Dia, fuso e horários](../dinheiro/fuso-dia-e-horarios.md) --- # Arquivo: docs/conceitos/glossario.md ## Glossário > Para: Dev · Fin · Adm — Conferido em 2026-10-18. Os termos que aparecem em mais de uma página do guia, em ordem alfabética dentro de cada grupo. Cada definição aponta para a página que explica o assunto por inteiro. ### O que é Quatro grupos: [contas e acesso](#contas-e-acesso), [chamadas à API](#chamadas-a-api), [dinheiro](#dinheiro) e [webhooks](#webhooks). ### Quando usar Quando um termo de outra página não está claro. Os seis termos de base (tenant, operação, tipo da operação, conector, jogador e PSP) também estão em [Visão geral](visao-geral.md). ### Como funciona #### Contas e acesso | Termo | Definição | |---|---| | **Administrador do tenant** | A primeira pessoa da sua empresa no painel. Entra por convite, convida a equipe e pede chaves. Veja [Equipe e papéis](../painel/equipe-e-papeis.md). | | **Aprovação de outra pessoa (4-eyes)** | Regra do painel: uma ação sensível vira um pedido, e só vigora depois que **outra pessoa** (nunca quem pediu) aprova. Um voto contra encerra o pedido. Veja [O painel](../painel/README.md#aprovacao-de-outra-pessoa-4-eyes-e-espera-time-lock). | | **Conector** | O serviço do seu lado que fala com o Dracma. Só ele guarda a chave e o segredo. | | **Confirmação recente (step-up)** | Prova de identidade com um fator forte nos últimos 5 minutos, exigida para aprovar, revelar segredo ou ver dado pessoal. Veja [Conta, login e dispositivos](../painel/conta-e-login.md). | | **Fator forte** | Passkey ou aplicativo autenticador. O e-mail sozinho não basta para as ações sensíveis. | | **Operação** | Uma marca em um país e em um ambiente. É a unidade de credencial, regra, moeda, webhook e saldo. | | **Pacote (papel)** | Conjunto de permissões de uma pessoa na operação: Administrador, Financeiro, Revisor ou Auditor. Veja [Equipe e papéis](../painel/equipe-e-papeis.md). | | **Staff do Dracma** | A equipe do Dracma que cria tenants, cadastra e ativa gateways e aprova pedidos de operação Real. | | **Tenant** | A sua empresa (pessoa jurídica) no Dracma. Agrupa pessoas, operações e a trilha de auditoria. | | **Time-lock (espera)** | Espera de 24 h depois da aprovação em certos pedidos (chave nova, IP novo, URL de webhook, afrouxar regra). Durante a espera, o pedido pode ser cancelado. | | **Tipo da operação** | **Teste** (simulador, sem dinheiro real, chave `key_test_`) ou **Real** (PSP de verdade, dinheiro real, chave `key_live_`). Veja [Ambientes, tipos de operação e hosts](ambientes-e-hosts.md). | #### Chamadas à API | Termo | Definição | |---|---| | **`external_id`** | O id que **você** dá a cada depósito ou saque. Único por operação e por tipo; é a âncora de idempotência e de conciliação. Veja [Idempotência e repetição](../api/idempotencia.md#external_id). | | **Escopo** | Permissão da chave: `read`, `deposits:write`, `withdrawals:write`, `rules:propose`. Veja [Escopos](../api/autenticacao.md#escopos). | | **`Idempotency-Key`** | Cabeçalho obrigatório em todo `POST`. Repetir com a mesma chave e o mesmo corpo devolve o mesmo resultado, sem novo efeito. Veja [a chave](../api/idempotencia.md#a-chave). | | **Leitor tolerante** | Código que ignora campo desconhecido e trata estado, evento ou código desconhecido como "não terminal", sem mover dinheiro. Veja [Convenções HTTP](convencoes-http.md#leitor-tolerante-e-evolucao). | | **`Money`** | Valor monetário como texto de inteiro na menor unidade (`"5000"` = R$ 50,00). Nunca número de ponto flutuante. Veja [Dinheiro](convencoes-http.md#dinheiro-money). | | **Nonce** | Valor aleatório de 32 hex minúsculos, novo em toda requisição assinada. O Dracma recusa um nonce repetido com `401 replayed_nonce`. | | **Pedido canônico** | O texto de 6 linhas que você assina com HMAC-SHA256. Veja [Pedido canônico e assinatura](../api/autenticacao.md#pedido-canonico-e-assinatura). | | **`problem+json`** | Formato dos erros da API (RFC 9457), com `code` estável, `retryable` e `trace_id`. Veja [Erros](../api/erros.md). | | **`Dracma-Trace-Id`** | Identificador da requisição, em toda resposta. É o que o suporte pede. Veja [Suporte](../ajuda/suporte.md). | | **Vetores de assinatura** | Casos normativos de assinatura de chamada e de webhook que o seu código deve reproduzir byte a byte. Veja [Exemplos executáveis](../exemplos/README.md). | #### Dinheiro | Termo | Definição | |---|---| | **`available`** | Dinheiro da operação que entrou por depósito pago e ainda não saiu. Inclui o valor reservado. Veja [Saldos e caixa livre](../dinheiro/saldos.md). | | **Caixa livre** | `available - pending_out`. É o que sustenta um novo saque. | | **`pending_in`** | Dinheiro recebido e ainda não creditado (depósitos em revisão). Informativo. | | **`pending_out`** | Valor reservado para saques que o Dracma está enviando ao PSP. | | **Decisão** | Resultado do motor de regras para um saque: automático, revisão ou bloqueio. Veja [Decisão, limites e regras](../dinheiro/decisao-limites-e-regras.md). | | **Digest** | Resumo assinado do relatório diário de reconciliação. Veja [Reconciliação](../referencia/reconciliacao.md#o-digest). | | **Gateway** | A conexão de uma operação com um PSP, para entrada (depósito) ou saída (saque). O cadastro é feito pelo staff. Veja [Gateways](../painel/gateways-e-psp.md). | | **`needs_decision`** | Caso que o seu conector abre quando o estado exige decisão humana (estorno, chargeback, devolução). Veja [Política de exceções](../conector/politica-de-excecoes.md). | | **PSP** | O provedor de pagamento que movimenta o PIX de fato. O conector nunca fala com ele. | | **Reconciliação** | Conferência diária do seu livro contra o relatório do Dracma. Veja [Reconciliar com o seu livro](../dinheiro/reconciliacao-do-operador.md). | | **Regras** | Conjunto versionado por operação com limites, horários e aprovação dupla, lido em `GET /v1/rules`. Veja [Regras (API)](../referencia/regras.md). | | **Reserva** | Débito que **você** faz no saldo do jogador antes de `POST /withdrawals`. Devolvida só em `rejected`, `failed` ou `cancelled`. | | **Revisão** | Decisão humana, no painel, sobre um depósito ou saque que as regras não liberaram sozinhas. Veja [Revisão](../painel/revisao-de-depositos-e-saques.md). | | **`under_review`** | Estado de depósito ou saque à espera da revisão. | | **Estorno, chargeback** | Devolução de um depósito já pago (`refunded`, `chargeback`). Só existem no sandbox hoje. Veja [Devoluções, estornos e disputas](../dinheiro/devolucoes-estornos-e-disputas.md). | #### Webhooks | Termo | Definição | |---|---| | **DLQ** | Fila das entregas que esgotaram as tentativas sem `2xx` em 72 h. O evento continua em `GET /events` e pode ser reentregue pelo painel. Veja [Retentativas, skipped e DLQ](../webhooks/retentativas-e-dlq.md). | | **Entrega** | O registro de um evento a enviar para a URL da operação. Há uma entrega por evento; cada tentativa é um `POST`. | | **Envelope** | O JSON do webhook: `id`, `type`, `object_id`, `object_version`, `data.object`. Veja [Envelope](../webhooks/envelope.md). | | **Fetch-back** | A consulta que o receptor faz à API, com a própria credencial, para confirmar o estado antes de mover dinheiro. O corpo do webhook nunca é a fonte de verdade. | | **`Dracma-Event-Id`** | Identificador estável de um evento (`evt_...`). É a chave de deduplicação do receptor. | | **`object_version`** | Contador que só cresce, por objeto. Resolve a ordem quando os eventos chegam fora de ordem. | | **Operador de referência** | Receptor que a suíte de conformidade usa como oráculo. Veja [Suíte de conformidade](../sandbox/conformidade.md). | | **`skipped`** | Entrega registrada e não enviada: sem URL cadastrada, ambiente diferente ou tipo não assinado. | | **Webhook** | `POST` assinado do Dracma para o seu servidor a cada mudança de estado. Só avisa. Veja [Webhooks](../webhooks/README.md). | ### Exemplos Na frase "o receptor deduplica pelo `Dracma-Event-Id`, faz fetch-back e credita pelo `external_id`", os três termos se encaixam: o evento chega (webhook), a consulta confirma o estado (fetch-back) e o seu livro grava uma vez (idempotência). ### Erros e casos de borda - **"Aprovação dupla"** tem dois sentidos neste guia. Nas ações do painel, basta **uma** aprovação de outra pessoa. Na revisão de um saque de valor alto, o motivo `DUAL_APPROVAL_REQUIRED` pede **duas** pessoas diferentes ([Revisão](../painel/revisao-de-depositos-e-saques.md)). - **"Dia"** é o dia UTC na reconciliação e o dia do fuso da operação nas janelas de limite ([Dia, fuso e horários](../dinheiro/fuso-dia-e-horarios.md)). ### Veja também - [Visão geral](visao-geral.md) - [Convenções HTTP](convencoes-http.md) - [Referência rápida](../referencia-rapida.md) - [FAQ](../ajuda/faq.md) --- # Arquivo: docs/onboarding/README.md ## Do zero ao go-live > Para: Adm · Dev — Conferido em 2026-10-18. Esta é a linha do tempo completa de um integrador: da aprovação comercial ao primeiro PIX real. Cada passo diz quem faz, onde e qual prova você guarda. Os detalhes de cada passo ficam nas páginas ligadas. ### O que é O onboarding junta três frentes que andam em paralelo: - **Comercial e conformidade (KYB):** acontece fora do painel e do código. O Dracma recebe os dados da empresa e decide se abre o tenant. Não há formulário de KYB no painel. - **Acesso:** o Dracma cria o tenant e convida o administrador da sua empresa por e-mail. Ele aceita, vira dono do tenant e convida o resto da equipe. - **Técnica:** você cria uma operação Teste, recebe a chave, liga o webhook, roda o quickstart e a suíte de conformidade, reconcilia um dia, pede a operação Real e vai ao ar. Só o staff do Dracma cria tenants. Não existe cadastro self-service de empresa. Quem cria o tenant **não recebe papel nele**: o dono é quem aceita o convite. A aprovação de uma operação Real é de **uma pessoa do Dracma que não seja quem fez o pedido**. O número de pessoas envolvidas na análise comercial não é uma regra do produto e não aparece aqui. ### Quando usar Leia primeiro esta página para ter o mapa. Depois siga a ordem: [O que preparar](pre-requisitos.md), [Primeiro depósito e primeiro saque](quickstart-sandbox.md), [De Teste para Real](promocao-para-producao.md) e o [checklist de go-live](../go-live/checklist.md). ### Como funciona #### Linha do tempo ```mermaid flowchart TD K["1. KYB e contrato (fora do painel)"] --> T["2. Dracma cria o tenant e convida o administrador por e-mail"] T --> A["3. Administrador aceita o convite e vira dono do tenant"] A --> E["4. Dono convida a equipe: pelo menos 2 pessoas com 2 fatores"] E --> O["5. Dono cria a operação Teste no hml e revela a chave"] O --> W["6. Webhook: URL e primeiro segredo"] W --> Q["7. Quickstart: primeiro depósito e primeiro saque"] Q --> C["8. Suíte de conformidade verde"] C --> R["9. Um dia reconciliado nos dois lados"] R --> P["10. Pedido de operação Real"] P --> V["11. Uma pessoa do Dracma aprova; chave key_live_ revelada uma vez"] V --> G["12. Gateway ativo na operação Real (cadastro e ativação pelo Dracma)"] G --> L["13. Limites conservadores e alertas"] L --> F["14. Primeira transação real de valor mínimo"] F --> X["15. Registro de go-live"] ``` #### Quem faz o quê | # | Passo | Quem | Onde | Prova que você guarda | |---|---|---|---|---| | 1 | KYB e contrato | Dracma e sua empresa | fora do painel | contrato assinado | | 2 | Criar o tenant e convidar o administrador | **Dracma** | painel (ação de plataforma) | e-mail de convite recebido | | 3 | Aceitar o convite | administrador da sua empresa | página de convite em `auth.` | acesso ao painel como dono | | 4 | Convidar a equipe | dono | painel, "Usuários e papéis" | lista de acessos | | 5 | Criar a operação Teste e revelar a chave | dono | painel, "Tenants e operações" | `GET /v1/ping` com `environment: "sandbox"` | | 6 | Cadastrar a URL do webhook e girar o primeiro segredo | administrador | painel, "Webhooks" | evento entregue no seu receptor | | 7 | Quickstart | dev | seu terminal | saídas dos comandos | | 8 | Conformidade | dev | [Suíte de conformidade](../sandbox/conformidade.md) | saída com todos os casos verdes e a data | | 9 | Reconciliar um dia | fin e dev | `GET /v1/reconciliation` | relatório comparado nos dois lados, sem divergência | | 10 | Pedir a operação Real | dono | painel | pedido registrado | | 11 | Aprovar o pedido | **Dracma** (outra pessoa) | painel | operação Real ativa e chave revelada | | 12 | Cadastrar e ativar o gateway | **Dracma** | painel | conexão ativa na tela "Gateways" | | 13 | Limites e alertas | fin e administrador | painel | regras ativas e alertas configurados | | 14 | Primeira transação real | fin e dev | painel "Cobrança de teste" ou API | depósito e saque do `POST` à linha da reconciliação | | 15 | Go-live | todos | [Dia do corte e rollback](../go-live/dia-do-corte-e-rollback.md) | registro assinado | #### Primeiro acesso: o administrador é convidado !!! note "Confirmar após deploy" O fluxo de convite do dono do tenant está em implantação. Os passos abaixo seguem o contrato aprovado. Confirme com o Dracma no onboarding se o seu ambiente já o usa. 1. O staff do Dracma cria o tenant informando o e-mail do administrador da sua empresa. Nenhuma conta do Dracma vira dona do tenant. 2. O administrador recebe um e-mail com um link de uso único, válido por 72 horas. **O e-mail é real, inclusive no hml.** Endereços dos domínios de teste (`test.dracma.cfd`, `.test`, `.invalid`, `.example`) nunca recebem e-mail de verdade. 3. O link abre a página de convite em `auth.`. A pessoa entra ou cria a conta **com o mesmo e-mail do convite**. A conta precisa de pelo menos 2 fatores, com um fator forte (passkey, ou senha mais app autenticador). Veja [Conta, login e dispositivos](../painel/conta-e-login.md#aceitar-o-convite-do-dono-do-tenant). 4. Ao aceitar, a pessoa vira **dona do tenant**. Só a partir daí ela cria operações e chaves. 5. O Dracma não guarda nem vê o link. Se o convite expirar, o staff reenvia (até 5 reenvios, com 60 s entre eles; cada reenvio invalida o link anterior). Consequências para você: - A primeira chave de uma operação Teste é revelada **a quem criou a operação**: a sua empresa, não o Dracma. O Dracma nunca recebe o segredo. - Se o convite não chegar, confira a caixa de spam e peça o reenvio ao Dracma. Não existe "esqueci meu convite" no painel. - O Dracma não fica com papel de dono no seu tenant. Em tenants criados antes do convite, a equipe do Dracma consta como dona; a saída dela é manual e acontece depois que o seu administrador aceita. #### Janelas de tempo que você precisa planejar | Ação | Espera | |---|---| | Aceitar o convite do dono | 72 h de validade do link | | Segunda chave de API (ou nova) | aprovação de **outra pessoa** da sua equipe e **24 h** de espera | | Revelar o segredo de uma chave nova | até **24 h** depois que a chave nasce, uma única vez | | Incluir IP na lista do conector | aprovação de outra pessoa e 24 h de espera | | Mudar URL ou eventos do webhook pelo painel | aprovação de outra pessoa e 24 h de espera | | Pedido de operação Real | aprovação de uma pessoa do Dracma, sem espera adicional | Por isso o go-live exige pelo menos **duas pessoas** na equipe: com uma só, nenhum pedido que precisa de aprovação fecha. ### Exemplos Um cronograma realista para quem prepara tudo antes: | Dia | O que acontece | |---|---| | D0 | Contrato assinado. Dracma cria o tenant e envia o convite | | D0 | Administrador aceita, convida uma segunda pessoa e cria a operação Teste. A chave aparece na hora | | D1 | Webhook ligado, quickstart e conformidade rodando | | D2 a D3 | Um dia de reconciliação (o relatório do dia só fecha em D+1, às 02:00 UTC) | | D3 | Pedido de operação Real, aprovação e revelação da chave | | D4 | Gateway ativo, limites, alertas e primeira transação real | | D5 | Go-live | Se você precisar de uma segunda chave ou de IP novo, some 24 h ao cronograma. ### Erros e casos de borda | Sintoma | Causa provável | O que fazer | |---|---|---| | Link de convite diz que expirou ou já foi usado | passou de 72 h, ou outra conta já o consumiu | peça reenvio ao Dracma | | Convite recusado por e-mail diferente | a conta usada não tem o e-mail do convite verificado | entre com o e-mail do convite ou adicione-o à conta e confirme o código | | Convite recusado por falta de fatores | a conta tem só o código por e-mail | cadastre uma passkey ou um app autenticador e repita | | "Você ainda não tem acesso a nenhuma operação" no painel | o seu usuário não tem pacote em nenhuma operação | peça ao administrador do seu tenant | | Operação aparece como "Configurando" | passos de provisionamento em andamento | aguarde; o painel conclui sozinho | | Chave não aparece | a janela de 24 h para revelar venceu | peça outra chave; veja [Chaves de API](../painel/chaves-de-api.md#revelar-o-segredo) | ### Veja também - [Visão geral](../conceitos/visao-geral.md) - [O que preparar](pre-requisitos.md) - [Equipe e papéis](../painel/equipe-e-papeis.md) - [Tenants e operações](../painel/tenants-e-operacoes.md) - [Checklist de go-live](../go-live/checklist.md) --- # Arquivo: docs/onboarding/pre-requisitos.md ## O que preparar > Para: Adm — Conferido em 2026-10-18. Reúna estes itens antes de pedir o tenant. Quase todos atrasam o go-live se chegarem depois: o convite precisa de um e-mail real, os pedidos do painel precisam de uma segunda pessoa e o webhook precisa de um domínio que o Dracma aceite. ### O que é Uma lista de dados que você entrega ao Dracma e de decisões que você toma antes do primeiro acesso. #### Dados da empresa (KYB) Entregue ao Dracma pelo canal combinado no contrato: - razão social e documento fiscal (CNPJ); - sócios e beneficiários finais; - licença ou autorização para operar no país; - contato comercial e contato técnico. #### Dados da integração | Item | Para que serve | Observação | |---|---|---| | E-mail do **administrador** do tenant | recebe o convite e vira dono | tem de ser um e-mail **real e monitorado** de uma pessoa. Não use caixa compartilhada nem endereço de teste | | Nome das operações | `marca-país-ambiente`, por exemplo `marca-a-br-hml` | só letras minúsculas, dígitos e hífen; 2 a 63 caracteres; começa com letra ou dígito | | IPs ou CIDRs de saída do conector | o formulário de nova operação pede pelo menos um | rede no mínimo `/24` (IPv4) e `/56` (IPv6); nunca `/0`; até 50 itens. A lista fica registrada, mas hoje não é aplicada às chamadas (veja [Rede, allowlist e mTLS](../seguranca/rede-allowlist-e-mtls.md)) | | URL do webhook | recebe os eventos de depósito e saque | `https`, sem usuário e senha, host público. Em hml, o domínio precisa ser reservado ou liberado pelo Dracma | | Domínio do conector de homologação | liberação do webhook no hml | peça no onboarding. Túneis temporários (ngrok e semelhantes) não funcionam | | Contatos de alerta | recebem e-mails de caixa baixo, webhook falhando e informação solicitada | informe ao Dracma; o painel não edita esses contatos | | Moedas e métodos | hoje `BRL` com `pix` | a operação nasce com BRL e PIX | !!! warning "Não disponível" O painel não permite cadastrar nem editar os contatos de alerta de e-mail da operação. Sem contatos, os alertas por e-mail e o aviso de "informação solicitada" não têm destinatário. Informe os e-mails ao Dracma no onboarding. ### Quando usar Antes do passo 2 da [linha do tempo](README.md): o Dracma só cria o tenant quando tem o e-mail do administrador. ### Como funciona #### Pelo menos duas pessoas, com dois fatores Várias ações do painel exigem **a aprovação de outra pessoa**: segunda chave de API, IP novo, mudança de URL do webhook, conceder um papel que aprova dinheiro, afrouxar uma regra, exportar dados pessoais. Quem pede nunca vota no próprio pedido. Com uma única pessoa, esses pedidos ficam parados. Por isso: 1. Tenha **pelo menos duas pessoas** com acesso ao painel já no go-live. Elas podem acumular papéis: quem administra o tenant também pode ser Financeiro (veja [Equipe e papéis](../painel/equipe-e-papeis.md)). 2. Cada pessoa precisa de **pelo menos dois fatores** de login e de um **fator forte** (passkey ou app autenticador) para aprovar, votar, revelar segredos e dados pessoais. 3. Cada pessoa usa um e-mail próprio. O e-mail identifica a conta, mas o identificador interno do usuário é um UUID que nunca muda. O primeiro administrador pode convidar o segundo sem aprovação (enquanto há um só administrador, o pedido dele vale na hora). A partir daí, conceder um papel que aprova dinheiro (Financeiro, Revisor) sempre pede a aprovação do outro administrador. #### Fuso da operação Cada operação tem um fuso, que define o "dia" das janelas de limite dos saques e o horário do saque automático. Uma operação criada pelo painel nasce com `America/Sao_Paulo`. O painel não altera o fuso depois. Se a sua operação precisa de outro fuso, peça ao Dracma antes de criar a operação. O relatório de reconciliação usa sempre o dia em UTC, independentemente do fuso da operação. Veja [Dia, fuso e horários](../dinheiro/fuso-dia-e-horarios.md). #### Webhook e domínio 1. Decida o endereço do receptor em produção (`https`) e o de homologação. 2. Para o de homologação, peça no onboarding a liberação do domínio do seu conector. 3. Tenha o receptor pronto para deduplicar por `Dracma-Event-Id` e conferir tudo por fetch-back. Veja [Receptor passo a passo](../webhooks/receptor.md). ### Exemplos Formulário preenchido para o Dracma (todos os valores são fictícios): | Campo | Valor | |---|---| | Administrador | `ana.admin@empresa-exemplo.example` (pessoa real, e-mail real em produção) | | Segunda pessoa | `bruno.fin@empresa-exemplo.example` | | Operações | `marca-a-br-hml`, `marca-a-br-prd` | | IPs de saída | `203.0.113.0/24` | | Webhook hml | `https://pagamentos-hml.marca-a.example/dracma` | | Webhook prd | `https://pagamentos.marca-a.example/dracma` | | Contatos de alerta | `pagamentos@empresa-exemplo.example` | ### Erros e casos de borda - **IP inválido ou mais de 50 itens:** o formulário recusa com "IP inválido". Informe `203.0.113.10` (vira `/32`) ou uma rede de no mínimo `/24`. - **Rede grande demais:** `/16` é recusada. A regra é de "rede mínima": o prefixo precisa ser `/24` ou mais longo em IPv4. - **Webhook em `http://`:** recusado em operação Real. - **Webhook em domínio de terceiro no hml:** a URL é recusada. Peça a liberação do domínio. - **Só uma pessoa na equipe:** o go-live não fecha. Convide a segunda pessoa antes. ### Veja também - [Do zero ao go-live](README.md) - [Conta, login e dispositivos](../painel/conta-e-login.md) - [Equipe e papéis](../painel/equipe-e-papeis.md) - [Tenants e operações](../painel/tenants-e-operacoes.md) --- # Arquivo: docs/onboarding/quickstart-sandbox.md ## Primeiro depósito e primeiro saque > Para: Dev — Conferido em 2026-10-18. Roteiro para ver o ciclo inteiro funcionando numa operação **Teste** antes de escrever o conector. Cada passo é um comando copiável com o resultado esperado. Nenhum dinheiro real se move: o PIX é sintético. Os comandos usam o cliente shell da pasta `exemplos/shell/` do guia (`bash`, `openssl`, `curl`) e `python3` só para ler JSON. O shell serve para **depurar**. Em produção use Go, Node ou PHP: comparação em tempo constante e segredo fora da linha de comando. ### O que é Uma sequência de dez passos: ping, moedas, depósito, pagamento simulado, fetch-back, eventos, primeiro saque, saque automático, queda simulada e reconciliação do dia seguinte. Ela exercita as regras que o seu conector precisa respeitar: idempotência, fetch-back antes de mover dinheiro e retomada sem duplicar. ### Quando usar Depois de criar a operação Teste e revelar a chave (passo 5 da [linha do tempo](README.md)) e antes de rodar a [suíte de conformidade](../sandbox/conformidade.md). ### Como funciona #### 0. Pré-requisitos - Chave `key_test_…` da operação Teste, com os escopos `deposits:write`, `withdrawals:write` e `read`, e o segredo num arquivo `0400`. - URL do webhook cadastrada na operação, em domínio reservado ou liberado pelo Dracma. Sem ela, os passos de webhook mostram só o log de entregas. - Relógio sincronizado por NTP: a janela de assinatura é de ±300 s. ```bash # Dentro da pasta exemplos/shell/ do guia. Ajuste a base e os caminhos ao que o onboarding entregou. export DRACMA_BASE_URL="https://api.dracma.cfd/v1" export DRACMA_KEY_ID="key_test_SEU_ID" export DRACMA_KEY_SECRET_FILE="/caminho/seguro/dracma-teste.key" # 0400, fora de qualquer repositório # dr: chamada assinada; imprime os headers da resposta em stderr e o corpo em stdout. dr() { bash ./assinar.sh "$@" | awk 'c{print;next} /^\r?$/{c=1;next} {print > "/dev/stderr"}'; } # js: lê um campo do JSON do stdin. Ex.: js '["id"]' js() { python3 -c 'import json,sys; v=json.load(sys.stdin); print(eval("v"+sys.argv[1]))' "$1"; } # novo_id: UUID para external_id e Idempotency-Key (em produção use UUIDv7) novo_id() { python3 -c 'import uuid; print(uuid.uuid4())'; } # T: pasta de trabalho temporária T="$(mktemp -d)" ``` Para ver a assinatura sem chamar a rede, prefixe a chamada com `SO_HEADERS=1`. #### 1. Ping: a chave caiu na operação certa? ```bash dr GET /ping ``` Esperado: `HTTP/… 200` e `{"ok":true,"operation_id":"","environment":"sandbox","server_time":"…"}`. Compare `server_time` com `date -u`: acima de 5 s, corrija o NTP. | Se vier | Causa provável | |---|---| | `401 unknown_key` | chave `key_test_` no host errado, formato inválido ou chave revogada | | `401 invalid_signature` | segredo errado, key id que não existe, caminho sem `/v1` ou nonce com maiúscula | | `401 timestamp_out_of_window` | relógio fora de ±300 s | #### 2. Moedas, métodos e exigências ```bash dr GET /currencies ``` Esperado: `BRL` com `methods: ["pix"]` e `method_details[0].deposit_requirements`. A lista diz quais campos do jogador são obrigatórios nesta operação. Mande sempre `player.name`. Com `player.email` na lista, o e-mail precisa ser de domínio reservado (`.test`, `.invalid`, `.example`) no Teste. #### 3. Primeiro depósito O corpo do depósito: ```json { "external_id": "dep_0192a3b4-5c6d-7e8f-9a0b-1c2d3e4f5a6b", "player": { "external_ref": "jogador-0001", "document": { "type": "CPF", "number": "52998224725", "country": "BR" }, "name": "Jogador Teste", "email": "jogador1@operador.test", "ip": "203.0.113.10" }, "amount": { "amount": "5000", "currency": "BRL" }, "method": "pix" } ``` `52998224725` é um CPF de teste: dígito verificador válido, sem titular real. Use CPFs gerados, nunca o de uma pessoa. ```bash DEP_EXT="dep_$(novo_id)" cat > "$T"/dep.json < "$T"/pagador.json # dr POST "/sandbox/deposits/$DEP_ID/pay" "$T"/pagador.json "sbx_$(novo_id)" → under_review, PAYER_MISMATCH ``` #### 5. Fetch-back: a única fonte de verdade ```bash dr GET "/deposits/$DEP_ID" | tee "$T"/dep-now.json | js '["status"], v["version"], v["amount"]' ``` Esperado: `('paid', 3, {'amount': '5000', 'currency': 'BRL'})`; a versão pode variar. **É aqui, e só aqui, que o conector credita**, depois de conferir `external_id`, `amount`, `currency` e `player_ref` com o que gravou. #### 6. Eventos e webhook ```bash dr GET "/events?object_id=$DEP_ID" # deposit.pending, deposit.paid, na ordem dr GET /sandbox/webhooks # log de entregas: evento, tentativa, status HTTP do seu receptor ``` Teste a deduplicação reentregando o `deposit.paid`: ```bash EVT=$(dr GET "/events?object_id=$DEP_ID" | js '["data"][-1]["id"]') echo "{\"event_id\":\"$EVT\"}" > "$T"/redeliver.json dr POST /sandbox/webhooks/redeliver "$T"/redeliver.json "sbx_$(novo_id)" ``` Esperado no seu receptor: mesmo `Dracma-Event-Id`, resposta `200`, **nenhum** crédito novo. #### 7. Primeiro saque Por padrão o primeiro saque de um jogador vai para revisão (`FIRST_WITHDRAWAL`); a regra da operação pode mudar isso. No conector, a reserva do saldo do jogador acontece **antes** deste `POST`. O saldo de caixa da operação para pagar saques vem dos depósitos já pagos, por isso o passo 5 vem antes. ```bash WDR_EXT="wdr_$(novo_id)" cat > "$T"/wdr.json <')`. No conector, finalize a reserva **só** ao ler `paid` no fetch-back. #### 8. Segundo saque: o caminho automático Com o primeiro saque pago, um saque dentro do limite que a regra da operação permite pode sair no automático: ```bash WDR2_EXT="wdr_$(novo_id)" sed "s/$WDR_EXT/$WDR2_EXT/" "$T"/wdr.json > "$T"/wdr2.json dr POST /withdrawals "$T"/wdr2.json "$WDR2_EXT" | js '["status"], v["decision"]["mode"], v["decision"]["reasons"]' ``` Esperado: `('approved', 'automatic', [])`. Se vier `under_review`, leia `decision.reasons`: as regras da operação (`GET /v1/rules`) podem pedir revisão por outros motivos. Sem `player.ip`, o resultado é sempre `under_review` com `OWNERSHIP_UNVERIFIED`. Para ver a devolução da reserva, crie outro saque e use `.../reject` (a partir de `under_review`) ou `.../fail` (a partir de `approved`), e confira `rejected` ou `failed` no fetch-back. #### 9. Queda simulada e retomada sem duplicar ```bash echo '{"down":true}' > "$T"/down.json echo '{"down":false}' > "$T"/up.json dr POST /sandbox/outage "$T"/down.json "sbx_$(novo_id)" DEP2_EXT="dep_$(novo_id)" sed "s/$DEP_EXT/$DEP2_EXT/" "$T"/dep.json > "$T"/dep2.json dr POST /deposits "$T"/dep2.json "$DEP2_EXT" # → 503 service_unavailable, com Retry-After dr POST /sandbox/outage "$T"/up.json "sbx_$(novo_id)" dr POST /deposits "$T"/dep2.json "$DEP2_EXT" # MESMA chave, MESMO corpo → 201 ou 202 dr POST /deposits "$T"/dep2.json "$DEP2_EXT" # → mesma resposta, Idempotent-Replayed: true dr GET "/deposits?external_id=$DEP2_EXT" | js '["data"].__len__()' # → 1 ``` É o comportamento que o outbox do seu conector precisa ter em produção. #### 10. No dia seguinte: reconciliação ```bash ONTEM=$(date -u -d yesterday +%F) dr GET "/reconciliation?date=$ONTEM¤cy=BRL&limit=200" ``` Esperado depois das 02:00 UTC de hoje: itens `deposit` e `withdrawal` com `status: paid`, `totals`, `digest` e o header `Dracma-Signature` na última página. Antes disso: `409 day_open` com `Retry-After`. Confira o digest com as funções dos exemplos Go ou Node. #### Limpeza ```bash rm -rf "$T" ``` ### Exemplos Os dois arquivos que o roteiro monta (`dep.json` e `wdr.json`) são os corpos mínimos de depósito e saque. Os campos obrigatórios e as exigências de cada operação estão em [Depósitos](../referencia/depositos.md) e [Saques](../referencia/saques.md). As demais receitas (revisão, devolução, expiração) estão em [Receitas por caminho](../sandbox/receitas.md). ### Erros e casos de borda | Sintoma | Causa | O que fazer | |---|---|---| | `400` com `reason: required_by_operation` | a operação exige um campo do jogador que você não mandou | leia `GET /currencies` e preencha | | `400` com `reason: reserved_domain_required` | e-mail de jogador de domínio não reservado em operação Teste | use `.test`, `.invalid` ou `.example` | | `404` em `/sandbox/...` | você está num host de operação Real | rode o roteiro no host de Teste | | `422 document_mismatch` | outro documento para o mesmo `external_ref` | use o mesmo documento ou outro `external_ref` de teste | | `409` ao criar um depósito com `external_id` já usado | o `external_id` é único na operação | consulte o depósito existente em vez de criar outro | ### Veja também - [Ambientes, tipos de operação e hosts](../conceitos/ambientes-e-hosts.md) - [Sandbox](../sandbox/README.md) - [Receitas por caminho](../sandbox/receitas.md) - [Suíte de conformidade](../sandbox/conformidade.md) - [De Teste para Real](promocao-para-producao.md) --- # Arquivo: docs/onboarding/promocao-para-producao.md ## De Teste para Real > Para: Adm · Dev — Conferido em 2026-10-18. Uma operação Teste nunca vira Real. Você pede uma **operação Real nova**, recebe uma chave `key_live_` nova e troca a configuração do conector. O código do conector é o mesmo. Esta página mostra o pedido, a revelação da chave, o gateway, o webhook, os limites e a primeira transação real. ### O que é A promoção é a troca de **duas operações**, cada uma com a própria chave, regra, saldo e webhook: | | Operação Teste | Operação Real | |---|---|---| | Onde | hml (`api.`) | prd (`api.` do domínio de produção) ou hml (`api-live.`) | | Chave | `key_test_…` | `key_live_…` | | PSP | simulador | PSP de verdade | | Dinheiro | nunca | real | | Rotas `/v1/sandbox/*` | abertas | `404` | | Webhook | URL de homologação | URL de produção, `https` | !!! danger "Dinheiro real" A operação Real move dinheiro real. Os testes do passo 14 pagam e sacam PIX de verdade, com valor mínimo. O que muda no conector ao promover: 1. `$DRACMA_BASE_URL` passa a apontar para o host da operação Real. 2. O key id e o segredo passam a ser os `key_live_`. 3. O segredo de webhook passa a ser o da operação Real (cada operação tem o seu). 4. A URL do webhook passa a ser a de produção. 5. A suíte de conformidade e os testes que chamam `/v1/sandbox/*` **não rodam** contra a operação Real: essas rotas respondem `404`. ### Quando usar Depois que o [checklist de go-live](../go-live/checklist.md) estiver cumprido na operação Teste: conformidade verde, receptor com fetch-back e deduplicação, um dia de reconciliação sem divergência. O Dracma pede esse registro antes de aprovar o pedido da operação Real. ### Como funciona #### 1. Pedir a operação Real O pedido é feito no painel, em "Tenants e operações", pelo **dono do tenant** (o administrador que aceitou o convite). Informe: - o tenant e o nome da operação (por exemplo `marca-a-br-prd`); - o ambiente **Real**; - os IPs ou CIDRs de saída do conector (o formulário pede pelo menos um); - a URL `https` do webhook de produção (opcional na criação, mas recomendada); - o **motivo**, de 10 a 1000 caracteres. O pedido exige confirmação recente com fator forte. Ele fica **pendente** até uma pessoa do Dracma, que não seja você, aprovar. ```mermaid sequenceDiagram participant D as Dono do tenant (você) participant P as Painel participant S as Staff Dracma D->>P: pede operação Real (motivo, IPs, URL, fator forte) P-->>D: pedido pendente S->>P: aprova (outra pessoa, fator forte) P->>P: cria a operação e provisiona regras, chave e webhook P-->>D: operação Real, chave disponível por 24 h D->>P: revela o segredo da chave (uma vez, fator forte) ``` Estados do pedido: **pendente**, **executado**, **recusado**, **cancelado** e **falhou**. Só quem pediu cancela. Quem pediu nunca vota. Os detalhes estão em [Tenants e operações](../painel/tenants-e-operacoes.md). #### 2. Revelar a chave `key_live_` Depois da aprovação, o painel mostra a operação como "Configurando" até o provisionamento terminar (alguns minutos, com novas tentativas automáticas). Então: 1. A chave aparece com o prazo para revelar: **24 horas**. 2. Quem **pediu** a operação clica em revelar, com fator forte. O segredo aparece **uma única vez**. 3. Grave o segredo no seu cofre e entregue ao conector pela sua ferramenta de deploy. Veja [Segredos e vazamento](../seguranca/segredos-e-credenciais.md#onde-guardar). Se o prazo vencer ou a tela for fechada, a chave não é mais revelável. Peça uma chave nova (aprovação de outra pessoa e 24 h de espera). Uma operação aceita até duas chaves ativas. #### 3. Gateway ativo A operação Real só cobra e paga depois que existe uma **conexão de gateway ativa**. Quem faz isso é o Dracma, não você: 1. O staff cadastra a credencial do PSP na operação. A conexão nasce **inativa**. 2. Uma segunda pessoa do Dracma aprova a ativação. 3. Você vê a conexão no painel, em "Gateways", **somente leitura**: PSP, direção (entrada ou saída), prioridade e se está ativa. Confirme na tela "Gateways" que a conexão está ativa **antes** do primeiro teste. Sem conexão ativa a operação Real não tem como cobrar. Veja [Gateways (só leitura)](../painel/gateways-e-psp.md) e [PSPs](../dinheiro/psps-e-ambientes.md). #### 4. Webhook de produção 1. A URL informada no pedido já vale na operação nova. Mudanças depois passam por aprovação de outra pessoa e 24 h de espera, pelo painel. 2. O **primeiro segredo de webhook só existe depois de "Rotacionar segredo"** na tela de webhooks. Quem tem permissão de gerir webhooks (administrador do tenant) o rotaciona; o valor aparece uma única vez. 3. Instale o segredo no receptor de produção. Veja [Segredo e rotação](../webhooks/segredos-e-rotacao.md#o-primeiro-segredo). #### 5. Limites conservadores e alertas A operação nasce com um conjunto de regras inicial. Antes da primeira transação real, deixe-o mais restritivo do que o necessário: 1. Leia a regra ativa com `GET /v1/rules` ou na tela "Regras e listas". Este guia não publica valores padrão: consulte a sua operação. 2. Proponha a mudança (pelo painel, com simulação prévia; ou por `POST /v1/rules/change-requests` com uma chave que tenha o escopo `rules:propose`). 3. **Endurecer** (limites menores, mais revisão) vale na hora. **Afrouxar** exige a aprovação de outra pessoa e 24 h de espera. 4. Configure os alertas de caixa baixo e de webhook falhando em "Alertas". Veja [Alertas e e-mails](../painel/alertas.md#configurar-uma-regra). #### 6. Primeira transação real Faça um depósito de **valor mínimo** pela tela "Cobrança de teste" (em operação Real ela pede a confirmação "é REAL") ou pela API, e acompanhe do `POST` ao crédito e à linha da reconciliação. Depois faça um saque de valor mínimo. !!! warning "Não disponível" O Dracma não permite abastecer o caixa de saques de outra forma que não seja por depósitos pagos (ou devoluções). Não existe aporte. O primeiro saque real só é possível depois de existir depósito pago na operação. Confira `GET /v1/balances`. ### Exemplos Prova de que a chave certa está no conector: `GET /v1/ping` com a chave nova devolve `environment: "production"` e o `operation_id` da operação Real. ```json { "ok": true, "operation_id": "0192a3b4-0000-7000-8000-0000000000b2", "environment": "production", "server_time": "2026-10-18T12:00:00Z" } ``` Usar a chave `key_test_` no host da operação Real, ou o inverso, volta `401 unknown_key`. ### Erros e casos de borda | O que você vê | Causa | O que fazer | |---|---|---| | Pedido fica pendente | ninguém do Dracma aprovou | avise o Dracma pelo canal do onboarding | | `self_review_forbidden` | quem pediu tentou votar | outra pessoa vota | | `reveal_expired` | passou das 24 h para revelar | peça outra chave | | `already_revealed` | o segredo já foi mostrado | use o que você guardou; se perdeu, peça outra chave | | `key_limit_reached` | já há duas chaves ativas | revogue uma que não é mais usada | | Operação "Configurando" por muito tempo | passo de provisionamento falhando | aguarde as novas tentativas automáticas; se passar bem de 10 minutos, abra chamado | | `environment_not_allowed` | tipo não permitido na instância | veja [Ambientes, tipos de operação e hosts](../conceitos/ambientes-e-hosts.md#matriz-instancia-tipo) | !!! warning "Não disponível" Editar ou apagar um tenant ou uma operação não é possível pelo painel. Peça ao suporte. ### Veja também - [Do zero ao go-live](README.md) - [Chaves de API](../painel/chaves-de-api.md) - [Cobrança de teste](../painel/cobranca-de-teste.md) - [Dia do corte e rollback](../go-live/dia-do-corte-e-rollback.md) --- # Arquivo: docs/painel/README.md ## O painel > Para: Adm · Fin — Conferido em 2026-10-18. O painel é a aplicação web em que a equipe do operador administra a própria integração com o Dracma: operações, chaves, webhooks, revisão de saques e depósitos, regras, caixa, reconciliação, auditoria, relatórios e alertas. Esta página mostra a estrutura comum a todas as telas e resume quais ações pedem aprovação, espera e confirmação de identidade. Cada tela tem a sua página neste guia. ### O que é O painel tem o endereço `painel.` e o login fica em `auth.`, uma aplicação separada (veja [Conta, login e dispositivos](conta-e-login.md) e [Ambientes, tipos de operação e hosts](../conceitos/ambientes-e-hosts.md)). Você não cria conta sozinho: o acesso nasce de um convite da equipe do Dracma (primeiro administrador) ou de um administrador do seu tenant (demais pessoas). O painel **não é uma API**. Tudo o que ele mostra e faz é para pessoas. Quem integra o sistema do operador usa a API HMAC descrita em [Autenticação HMAC](../api/autenticacao.md). Este guia responde à pergunta "existe um guia do painel do operador?": sim, são as páginas desta seção. | Área do menu | Tela | Página do guia | |---|---|---| | Início | Operações | esta página | | Operação | Transações | [Transações](transacoes.md) | | Operação | Depósitos em revisão e Saques em revisão | [Revisão](revisao-de-depositos-e-saques.md) | | Operação | Saldos e caixa | [Saldos e caixa](saldos-e-caixa.md) | | Operação | Cobrança de teste | [Cobrança de teste](cobranca-de-teste.md) | | Regras | Regras e listas | [Regras, listas e emergência](regras-e-listas.md) | | Integração | Chaves e IPs | [Chaves de API](chaves-de-api.md) e [IPs do conector](allowlist-de-ip.md) | | Integração | Webhooks | [Webhooks no painel](webhooks.md) | | Plataforma | Tenants e operações | [Tenants e operações](tenants-e-operacoes.md) | | Plataforma | Gateways | [Gateways (só leitura)](gateways-e-psp.md) | | Plataforma | Reconciliação | [Reconciliação no painel](reconciliacao.md) | | Administração | Usuários e papéis | [Equipe e papéis](equipe-e-papeis.md) | | Administração | Auditoria | [Auditoria](auditoria.md) | | Administração | Alertas | [Alertas e e-mails](alertas.md) | | Administração | Relatórios | [Relatórios](relatorios.md) | Alguns itens do menu existem só para a equipe do Dracma e não fazem parte deste guia. !!! warning "Não disponível" O painel não tem tela de uso, consumo, tarifas ou faturamento. A conferência financeira se faz pela [Reconciliação no painel](reconciliacao.md) e pelos [Relatórios](relatorios.md). Veja [Tarifas](../dinheiro/tarifas.md#uso-e-consumo). ### Quando usar Abra o painel para: - decidir saques e depósitos que as regras mandaram para revisão; - acompanhar saldo, caixa e divergências de reconciliação; - criar operações, chaves, endereços de webhook e alertas; - propor mudanças de regra e conferir a trilha de auditoria. A tela **Início** mostra um cartão por operação a que você tem acesso. Cada cartão traz o nome, o tipo (`produção` ou `sandbox`), os saldos **Disponível**, **A receber** e **A pagar** e dois botões com o tamanho das filas: **Saques em revisão** e **Depósitos em revisão**. Os números são os do livro do Dracma; a definição de cada saldo está em [Saldos e caixa livre](../dinheiro/saldos.md). ### Como funciona #### Barra superior 1. **Seletor de operação.** Trocar a operação mantém a mesma tela. Ele lista só as operações a que você tem acesso. A última operação aberta fica lembrada no navegador, por cerca de 180 dias, para o painel abrir direto nela. 2. **Selo de ambiente.** Mostra em que instância você está: - `PRD · produção`, em destaque, na instância de produção; - `HML · sandbox`, na instância de homologação; - `DEV · local`, em desenvolvimento. 3. **Selo de sessão.** Mostra `Sessão com fator forte` ou `Sessão básica`. Ações sensíveis só passam com fator forte recente (veja abaixo). 4. **Sair.** Encerra a sessão no painel e no provedor de identidade. !!! danger "Dinheiro real" O selo `HML · sandbox` descreve a instância, não o tipo da operação. Uma operação **Real** criada na homologação usa gateway de verdade e move dinheiro real. Confira sempre o tipo da operação (Teste ou Real) no seletor, e não só o selo. O mesmo conceito aparece com dois nomes. Em **Tenants e operações** o tipo é `Teste` ou `Real`; no seletor, nos cartões do Início e na cobrança de teste ele aparece como `sandbox` ou `produção`. `Teste` é `sandbox` e `Real` é `produção`. #### Isolamento entre operações Cada operação é um espaço separado. O endereço de cada tela leva o identificador da operação (`//chaves`, por exemplo), e abrir o endereço de uma operação a que você não tem acesso mostra "Não encontrado", sem confirmar que a operação existe. A permissão vale por operação: a mesma pessoa pode ser Financeiro numa operação e só Auditor em outra. #### Confirmação de identidade (step-up) Toda ação sensível exige **fator forte recente**: passkey ou aplicativo autenticador confirmados nos últimos 5 minutos. Se a confirmação venceu, o formulário mostra o link **Confirmar identidade agora**, que leva ao login do provedor e volta à mesma tela. Ações que só leem dados e algumas ações restritivas (cancelar um pedido, por exemplo) não pedem a confirmação. #### Aprovação de outra pessoa (4-eyes) e espera (time-lock) Mudanças que ampliam o risco viram um **pedido**. O pedido só vigora depois de aprovado por **outra pessoa** (nunca por quem propôs) e, em alguns casos, depois de uma espera de 24 h. Em qualquer pedido: - quem propôs não vota no próprio pedido; - um voto contra encerra o pedido como recusado; - cancelar um pedido que ainda não vigorou é permitido (nos pedidos de operação Real, só quem propôs cancela); - todo voto, proposta e cancelamento pede um motivo de 10 a 1000 caracteres e fica na [Auditoria](auditoria.md). Por isso cada tenant precisa de pelo menos **duas pessoas** com dois fatores de autenticação: com uma só pessoa, nenhuma aprovação fecha. Veja [Equipe e papéis](equipe-e-papeis.md). #### Ação, aprovação, espera e confirmação | Ação | Quem faz | Aprovação de outra pessoa | Espera | Confirmação recente | |---|---|---|---|---| | Criar tenant | equipe do Dracma | não | não | sim | | Criar operação Teste | dono do tenant ou equipe do Dracma | não | não | sim | | Criar operação Real | dono do tenant ou equipe do Dracma (pede) | 1 pessoa da equipe do Dracma | não | sim | | Pedir chave nova | Administrador do tenant | 1 | 24 h | sim | | Revelar o segredo da chave | quem pediu a chave | não | uma vez, em até 24 h | sim | | Revogar chave | Administrador do tenant | não | não | sim | | Incluir IP | Administrador do tenant | 1 | 24 h | sim | | Remover IP | Administrador do tenant | não | não | sim | | Mudar URL ou eventos do webhook | Administrador do tenant | 1 | 24 h | sim | | Rotacionar o segredo do webhook | Administrador do tenant | não | não (o segredo antigo vale mais 7 dias) | sim | | Reentregar ou resolver a DLQ | Administrador do tenant | não | não | não | | Alterar conexão de gateway | equipe do Dracma | 1 | não | sim | | Gerar cobrança de teste ou simular pagamento | Administrador, Financeiro | não | não | sim | | Decidir saque ou depósito | Financeiro, Revisor | um voto contra decide; a favor, 1 pessoa (2 se exigir aprovação dupla) | não | sim | | Revelar dados pessoais (no detalhe da revisão) | Financeiro, Revisor | não | não | sim | | Pedir informação ao operador | Financeiro, Revisor | não | não | não | | Mudança de regra restritiva | propõe: Administrador, Financeiro; aprova: Financeiro | 1 | não | sim | | Mudança de regra relaxante | idem | 1 | 24 h por padrão | sim | | Incluir entrada em lista | Financeiro | não | não | sim | | Remover entrada de lista ou ativar VIP | Financeiro | 1 | sim | sim | | Suspender o automático (emergência) | Administrador, Financeiro | confirmação de outra pessoa em até 24 h | não | sim | | Definir limiar de caixa | Financeiro | não | não | sim | | Resolver divergência de reconciliação | Financeiro | não | não | sim | | Exportar auditoria assinada | Administrador, Financeiro, Auditor | não | não | sim | | Exportar relatório mascarado | Administrador, Financeiro, Auditor | não | não | não | | Exportar relatório com dados pessoais | Financeiro, Auditor | 1 | não | sim | | Configurar alertas | Administrador, Financeiro | não | não | não | As ações de equipe (convidar, conceder e remover papéis) têm regras próprias: veja [Equipe e papéis](equipe-e-papeis.md). #### Idioma, fuso e acessibilidade - O painel é todo em português do Brasil. - Datas e horas aparecem no fuso `America/Sao_Paulo`. A [Reconciliação no painel](reconciliacao.md) trabalha por **dia UTC** e avisa isso na tela. Os filtros de data da [Auditoria](auditoria.md) também valem no horário de Brasília. - Valores aparecem em reais (`R$ 1.234,56`); os arquivos exportados usam inteiros na menor unidade, como a API. - O estado de cada item aparece sempre em texto, nunca só em cor; há um atalho **Pular para o conteúdo** no início de cada página. #### Faixa "Saques em verificação" Quando o Dracma põe os saques novos de uma operação em verificação, uma faixa aparece no alto de todas as telas dessa operação: os novos saques podem demorar mais que o normal para sair, e os depósitos seguem normalmente. A faixa não traz motivo nem detalhe; em caso de dúvida, acione o suporte informando o horário que a faixa mostra. ### Exemplos Rotina típica do Financeiro, no começo do dia: 1. Abra **Início** e veja se alguma operação mostra saques ou depósitos em revisão. 2. Entre em **Saques em revisão**, abra cada item e decida (veja [Revisão](revisao-de-depositos-e-saques.md)). 3. Abra **Saldos e caixa** e confira o disponível projetado de cada moeda (veja [Saldos e caixa](saldos-e-caixa.md)). 4. Depois das 02:00 UTC, abra **Reconciliação** e confira o dia anterior (veja [Reconciliação no painel](reconciliacao.md)). Se você abrir uma tela que precisa de operação (por exemplo **Chaves e IPs**) sem escolher uma, o painel abre direto na última operação usada, ou na primeira da lista. ### Erros e casos de borda - **"Você ainda não tem acesso a nenhuma operação. Peça ao administrador do seu tenant."** Aparece no Início e nas telas de operação quando a sua conta existe, mas nenhum papel foi concedido. Peça a um Administrador do tenant que conceda um pacote (veja [Equipe e papéis](equipe-e-papeis.md)). - **"Não encontrado. O item não existe ou você não tem acesso a ele."** O painel não distingue "não existe" de "sem permissão". Confira o seletor de operação e o seu papel. - **Itens do menu que levam a "Não encontrado".** A maioria dos itens aparece no menu para todos, e cada tela confere o seu papel ao abrir. **Tenants e operações** só aparece para quem é dono de algum tenant ou para a equipe do Dracma. - **Sessão vencida.** O painel leva ao login e volta para a tela em que você estava. - **"Atualizando a página…".** A página foi aberta antes de uma atualização do painel; ela recarrega sozinha uma vez. - **"Algo deu errado".** Falha inesperada. O botão **Tentar de novo** repete a tela. Se persistir, acione o suporte com o horário (UTC) e o que você estava fazendo. - **Falha ao entrar.** A página de entrada mostra "O login foi cancelado ou recusado", "O pedido de login expirou" ou "Não foi possível concluir o login". Tente de novo a partir do botão **Entrar**. A lista completa de mensagens está em [Mensagens do painel](erros-do-painel.md). ### Veja também - [Conta, login e dispositivos](conta-e-login.md) - [Equipe e papéis](equipe-e-papeis.md) - [Tenants e operações](tenants-e-operacoes.md) - [Mensagens do painel](erros-do-painel.md) - [Ambientes, tipos de operação e hosts](../conceitos/ambientes-e-hosts.md) - [Do zero ao go-live](../onboarding/README.md) --- # Arquivo: docs/painel/conta-e-login.md ## Conta, login e dispositivos > Para: Dev · Fin · Adm — Conferido em 2026-10-18. Toda pessoa que usa o painel entra pela página de login do Dracma (`auth.`), que é uma aplicação separada do painel. Esta página mostra como criar a conta, os métodos de login, a duração da sessão, a confirmação de identidade para ações sensíveis, os fatores, a recuperação de acesso e o controle de dispositivos. ### O que é - O login mora em `auth.` (veja [Ambientes e hosts](../conceitos/ambientes-e-hosts.md)). O painel em `painel.` **não pede senha**: ele redireciona você ao `auth.` e volta com uma sessão, pelo protocolo OIDC (código de autorização com PKCE). - Os tokens ficam só no servidor do painel. O navegador recebe um cookie opaco, `HttpOnly`. - O painel exige **dois fatores** de login e um **fator forte** (passkey ou app autenticador) para ações sensíveis, como votar, aprovar, revelar segredo ou dado pessoal e mudar configuração. O painel e a API conferem de novo, uma depois da outra, cada ação sensível. - A identidade de uma pessoa é um **identificador interno imutável** (um UUID). O e-mail não é identidade: uma pessoa pode ter vários e-mails, e trocar o e-mail não muda quem ela é. ### Quando usar Leia antes do seu primeiro acesso e quando for configurar a segurança da sua conta. Quem administra a equipe lê também [Equipe e papéis](equipe-e-papeis.md). ### Como funciona #### Criar a conta (primeiro acesso) Quem foi convidado cria a conta pela página **Criar conta** (`/cadastro`), usando **o mesmo e-mail do convite**: 1. **Etapa 01, e-mail:** informe o e-mail do convite. 2. **Etapa 02, senha:** escolha uma senha de 10 ou mais caracteres que não apareça em vazamentos conhecidos, ou escolha **Continuar sem senha** para entrar só com código por e-mail. 3. **Etapa 03, código por e-mail:** digite o código de 6 dígitos que chegou ao e-mail. 4. A conta só nasce quando o e-mail é confirmado pelo código. Quem foi convidado cai na mesma conta que o convite reservou. 5. Depois do cadastro, abra **Conta** e cadastre uma passkey ou um app autenticador. Sem fator forte você não consegue aprovar nem revelar nada no painel. Um e-mail já verificado em outra conta recebe "Este e-mail já tem conta. Use Entrar". #### Aceitar o convite do dono do tenant !!! note "Confirmar após deploy" O convite do dono do tenant está em implantação. O texto abaixo segue o contrato aprovado; confirme com o Dracma se o seu ambiente já o usa. O administrador da sua empresa recebe do Dracma um e-mail com um link do tipo `https://auth./convite#…`: 1. O link é de **uso único** e vale **72 horas**. O código do convite vai depois do `#`, então não aparece em logs de servidor e a página o apaga do endereço assim que abre. 2. Entre ou crie a conta **com o e-mail do convite**. Se a conta já existe com outro e-mail, adicione o e-mail do convite e confirme com o código. 3. O aceite exige **pelo menos dois fatores e um fator forte recente**: passkey, ou senha mais app autenticador. Conta que só usa código por e-mail precisa antes cadastrar passkey ou app autenticador. 4. Ao aceitar, a conta vira **dona do tenant** e a página leva ao painel. Quem criou o convite não pode aceitá-lo, e contas com papel de plataforma do Dracma não aceitam. O Dracma não vê o link: ele só vai por e-mail, mesmo no hml. | Resposta do convite | Significado | |---|---| | link não encontrado ou malformado | digitação errada ou convite inexistente (a mesma resposta para os dois casos) | | convite expirado, já usado ou revogado | peça o reenvio ao Dracma. Cada reenvio invalida o link anterior (até 5, com 60 s entre eles) | | e-mail diferente | a conta não tem o e-mail do convite verificado | | fatores insuficientes | cadastre passkey ou app autenticador e repita | #### Entrar O login tem três etapas: 1. **Etapa 01, e-mail.** Marque **Confiar neste dispositivo (sessão de 90 dias)** se o aparelho é seu. 2. **Etapa 02, 1º método.** 3. **Etapa 03, 2º método.** Uma conta com senha precisa de **senha mais código por e-mail**, em qualquer ordem. Uma passkey sozinha conclui o login e já vale como fator forte. A resposta é a mesma para e-mail existente e inexistente, para não revelar quem tem conta. #### Métodos oferecidos | Método | O que é | |---|---| | **Passkey** | Digital, rosto ou chave de segurança do dispositivo. Exige verificação do usuário. É um fator forte | | **Senha** | A senha da conta, com 10 ou mais caracteres | | **Código por e-mail** | 6 dígitos, válidos por 10 minutos, com 5 tentativas. O reenvio espera 30 s e cada e-mail recebe até 5 códigos por hora | | **App autenticador** | Código de 6 dígitos de um app TOTP. É um fator forte | | **Código de recuperação** | Um dos 10 códigos de uso único que você guardou | !!! warning "Não disponível" O Dracma não envia código por SMS nem por ligação, e não aceita telefone como identificador ou fator. A rota de SMS responde `404`. Use passkey, app autenticador ou código por e-mail. #### Sessão - A sessão dura **7 dias deslizantes**: cada uso renova o prazo. - Com **Confiar neste dispositivo**, a sessão dura **90 dias**. - O token de acesso dura 10 minutos e é renovado em segundo plano pelo servidor do painel. Você não é "chutado" a cada 15 minutos. - Um login de IP novo ou de dispositivo novo é tratado como mais arriscado: quem tem fator forte precisa apresentá-lo além de senha e código. #### Confirmação de identidade (step-up) Ações sensíveis pedem que você tenha confirmado a identidade com um **fator forte nos últimos 5 minutos**. São elas, entre outras: criar tenant ou operação, pedir ou revelar chave, mudar IPs ou webhook, rotacionar segredo, convidar ou remover pessoas, votar em pedidos, revelar dado pessoal e exportar relatórios. - Quando falta a confirmação, a tela mostra **Confirmar identidade agora**. Ela leva ao login, pede o fator forte e volta para a tela em que você estava. - Se a conta ainda não tem fator forte, o código por e-mail confirma que é você e o login oferece **Cadastrar passkey** ou **Cadastrar app autenticador** na hora. - A API repete a conferência: mesmo que alguém adultere a tela, a ação é recusada com `step_up_required`. #### Fatores, códigos de recuperação e e-mails A página **Conta** mostra: - seus e-mails e se cada um está verificado; - se a conta tem senha; - os **fatores fortes** cadastrados (passkey e app autenticador), com a data. Você cadastra e remove fatores aqui; - os **códigos de recuperação**, 10 de uso único, mostrados uma vez quando gerados. Regras: - Mudar fator, e-mail, senha ou códigos exige verificar de novo com **um fator diferente** do que está sendo mudado, no maior nível disponível (passkey, depois app autenticador, depois código por e-mail), nos últimos 5 minutos. - Toda mudança avisa **todos** os e-mails verificados da conta. - Remover o **último** fator forte não é imediato: a remoção fica agendada por **24 horas** e pode ser cancelada pelo link do aviso. Isso impede que quem roubou uma sessão apague seu fator. - Pessoas que aprovam dinheiro precisam ter pelo menos um fator forte. A tela avisa: "Eles são exigidos para aprovar pagamentos no painel". #### Dispositivos conectados A seção **Dispositivos conectados** lista cada sessão com o nome do navegador, o IP, o último uso e as marcas **este** e **confiável**. - **Encerrar** derruba uma sessão que não seja a atual. - **Sair de todos os dispositivos** encerra todas, inclusive a atual. - **Sair deste** encerra só a sessão atual. #### Sair (logout) Sair no painel é **irreversível**: apaga a sessão no servidor do painel, revoga o token de renovação, encerra a sessão no login e bloqueia os tokens de acesso ainda vivos daquela sessão. Não basta apagar o cookie para voltar: é preciso entrar de novo com os fatores. ### Exemplos ```mermaid sequenceDiagram participant N as Navegador participant P as painel. participant A as auth. (login) N->>P: abre o painel P-->>N: redireciona ao login (código de autorização + PKCE) N->>A: e-mail, 1º método, 2º método A-->>N: volta ao painel com um código N->>P: entrega o código P->>A: troca o código por tokens (no servidor) P-->>N: cookie opaco, tokens ficam no servidor ``` ### Erros e casos de borda #### Recuperar o acesso Há dois caminhos, ambos com força igual ou maior que a do login: 1. **Com código de recuperação:** use um dos 10 códigos **mais** um código por e-mail. 2. **Sem código:** peça a recuperação com o código por e-mail. Todos os e-mails da conta recebem um aviso, que você pode cancelar. Depois de **24 horas**, um novo código por e-mail conclui a recuperação. A sessão que sai de uma recuperação não vale como fator forte: ações sensíveis ainda pedem passkey ou app autenticador. #### Bloqueios e limites - Código por e-mail: 5 tentativas, depois o código morre. Reenvio a cada 30 s e até 5 códigos por hora por e-mail. - Senha errada várias vezes seguidas bloqueia a conta por alguns minutos. Credencial errada, conta inexistente e conta bloqueada mostram a **mesma** resposta e levam o mesmo tempo. - Muitos pedidos do mesmo IP ou da mesma conta levam a "Muitas tentativas. Aguarde alguns minutos". #### Mensagens do login e do cadastro | Mensagem | O que significa | O que fazer | |---|---|---| | "Digite um e-mail válido." | formato inválido | corrija | | "O tempo para entrar expirou. Comece de novo." | o fluxo passou de 15 minutos | recomece | | "Código incorreto ou expirado." | código errado, vencido ou já usado | peça outro | | "E-mail, senha ou código não conferem." | credencial errada | tente de novo | | "Muitas tentativas. Aguarde alguns minutos." | limite de tentativas | espere | | "Este e-mail já tem conta. Use Entrar." | e-mail já verificado em outra conta | entre | | "Senha fraca: use 10 ou mais caracteres que não apareçam em vazamentos conhecidos." | senha fora da política | escolha outra | | "Esta senha apareceu em vazamentos conhecidos. Escolha outra." | senha vazada | escolha outra | | "Não conseguimos enviar o código agora. Tente de novo em instantes." | o envio de e-mail falhou | tente de novo | #### E-mail no hml No hml, o código de login e os convites chegam por **e-mail de verdade** a qualquer endereço, exceto os de domínios de teste (`test.dracma.cfd`, `.test`, `.invalid`, `.example`), que nunca recebem e-mail. Use no hml o e-mail real de uma pessoa real. Contas de teste automatizado devem usar endereço de teste. Veja [Dados de teste](../sandbox/dados-de-teste.md#e-mail-real-no-hml-painel-e-convites). ### Veja também - [Equipe e papéis](equipe-e-papeis.md) - [O painel](README.md) - [Mensagens do painel](erros-do-painel.md) - [Rede, allowlist e mTLS](../seguranca/rede-allowlist-e-mtls.md) --- # Arquivo: docs/painel/equipe-e-papeis.md ## Equipe e papéis > Para: Adm — Conferido em 2026-10-18. Quem usa o painel recebe um ou mais **pacotes de papéis**, por operação. Esta página lista os quatro pacotes e o que cada um pode fazer, mostra como convidar, conceder e remover pessoas, e explica as regras de aprovação que impedem que uma pessoa sozinha faça uma mudança sensível. ### O que é Existem quatro pacotes. Os nomes são os da tela "Usuários e papéis". | Pacote | Para quem | Em uma frase | |---|---|---| | **Administrador do tenant** | quem comanda a conta da empresa | gere equipe, chaves, IPs e webhooks, cria operações e vê quase tudo | | **Financeiro** | quem opera o dinheiro | revisa saques e depósitos, aprova regras e listas, reconcilia e vê o caixa | | **Revisor** | quem decide filas de revisão | decide depósitos e saques em revisão e pede informação ao operador | | **Auditor** | quem confere | lê tudo, vê a trilha e exporta, mas não decide nada | O pacote **Administrador do tenant** vale para o tenant inteiro: quem o recebe passa a ser dono do tenant e cria operações. Os outros três valem para a operação em que foram concedidos. Uma pessoa pode ter mais de um pacote: quem administra pode também ser Financeiro ou Revisor. #### O que cada pacote pode fazer | Capacidade | Administrador do tenant | Financeiro | Revisor | Auditor | |---|:-:|:-:|:-:|:-:| | Ver a operação, transações e alertas | sim | sim | sim | sim | | Ver a trilha de auditoria | sim | sim | sim | sim | | Gerir usuários e papéis | sim | não | não | não | | Gerir chaves de API e IPs | sim | não | não | não | | Ver webhooks | sim | sim | não | sim | | Gerir webhooks (URL, eventos, segredo) | sim | não | não | não | | Criar operações no tenant | sim | não | não | não | | Cobrança de teste | sim | sim | não | não | | Decidir depósitos e saques em revisão | não | sim | sim | não | | Pedir informação ao operador | não | sim | sim | não | | Revelar dado pessoal, com motivo | não | sim | sim | sim | | Ver regras | sim | sim | não | sim | | Propor mudança de regra | sim | sim | não | não | | Aprovar regras, gerir listas e VIP | não | sim | não | não | | Suspender o saque automático (emergência) | sim | sim | não | não | | Ver a reconciliação | sim | sim | não | sim | | Agir na reconciliação e definir o limiar de caixa | não | sim | não | não | | Exportar relatórios sem dados pessoais | sim | sim | não | sim | | Exportar relatórios com dados pessoais (com aprovação) | não | sim | não | sim | | Configurar alertas | sim | sim | não | não | Quem não tem a capacidade não vê a tela: o painel responde "não encontrado" em vez de "proibido". #### Papéis do lado Dracma Duas funções existem só do lado do Dracma. Você não as concede nem as vê na lista de usuários: - **Operações do Dracma:** cria tenants, cadastra e ativa gateways e aprova pedidos de operação Real. - **Suporte do Dracma:** leitura dos gateways. Não aprova nada. Contas com função do Dracma não podem aceitar o convite de dono de um tenant de cliente. ### Quando usar Logo depois de aceitar o convite do dono: monte a equipe antes de pedir qualquer chave extra ou a operação Real. Volte aqui sempre que alguém entrar, mudar de função ou sair da empresa. ### Como funciona Abra **Usuários e papéis** e escolha a operação. Você precisa ser Administrador do tenant, e todas as ações pedem confirmação recente com fator forte (veja [Conta, login e dispositivos](conta-e-login.md)). #### Convidar por e-mail 1. Informe o e-mail da pessoa, o pacote e um motivo de 10 a 1000 caracteres. 2. O Dracma cria a conta da pessoa com o e-mail ainda **não verificado** e envia o convite. 3. A pessoa abre a página **Criar conta** e conclui o cadastro **com o mesmo e-mail**. O cadastro adota a conta que o convite reservou; o pacote já está concedido nela. 4. Convidar de novo alguém que já tem conta funciona: a pessoa mantém a mesma conta. No hml o e-mail chega de verdade a qualquer endereço fora dos domínios de teste. Use endereços reais de pessoas reais. #### Conceder um pacote a quem já tem acesso 1. Pegue o **ID interno** (UUID) da pessoa na tabela "Quem tem acesso". A tabela mostra o ID, o e-mail mascarado (`an***@dominio.example`) e os pacotes. 2. Informe o ID, o pacote e o motivo. A concessão é feita pelo **ID**, nunca pelo e-mail. #### Remover um pacote Na linha da pessoa, preencha o motivo e escolha **Remover pacote**. A remoção vale por pacote: se a pessoa tem dois, remova cada um. #### Regras de aprovação Toda ação vira um **pedido**. Quantas aprovações de **outra pessoa** ele precisa depende do pacote e de quantos administradores o tenant tem: | Ação | Um único administrador | Dois ou mais administradores | |---|---|---| | Convidar ou conceder **Administrador** ou **Auditor** | vale na hora, sem segundo voto | 1 aprovação de outro administrador | | Convidar ou conceder **Financeiro** ou **Revisor** (pacotes que aprovam dinheiro) | 1 aprovação de outro administrador, então só fecha com o segundo administrador | 1 aprovação de outro administrador | | Remover qualquer pacote | vale na hora | 1 aprovação de outro administrador | Pontos de atenção: - **Quem pede nunca vota no próprio pedido.** O voto de aprovação precisa ser de outro administrador, com fator forte recente. - Um voto de aprovação basta. Uma recusa fecha o pedido. - Quem administra o tenant **pode também ser Financeiro ou Revisor**. Não há separação obrigatória de funções. Com duas pessoas, os dois papéis cabem nas duas. - **Nunca se remove o último administrador:** o pedido volta `last_admin`. - Pedidos de usuários **não têm espera de 24 h**: ao receber a aprovação, o pedido é executado. - Os pedidos aparecem em "Pedidos pendentes desta área" e, depois de decididos, em "Histórico recente". Um pedido pendente pode ser cancelado. Sequência mínima para uma equipe de duas pessoas: 1. A primeira pessoa (dona do tenant) convida a segunda como **Administrador**: vale na hora. 2. Cada uma pode conceder à outra os pacotes **Financeiro** e **Revisor**, com a aprovação da outra. ### Exemplos Equipe de quatro pessoas de uma operadora: | Pessoa | Pacotes | Por quê | |---|---|---| | Ana | Administrador do tenant, Financeiro | dona do tenant e quem aprova regras | | Bruno | Administrador do tenant, Revisor | segundo administrador e quem decide as filas | | Carla | Revisor | turno da noite | | Davi | Auditor | compliance, leitura e exportação | Para dar o pacote Revisor à Carla: Ana pede (informando o e-mail), Bruno aprova, a Carla cria a conta e entra. O "administrador do seu tenant" que o painel cita ao ver a tela sem acesso é a pessoa dona do tenant. Ela é a primeira da lista: entrou por convite do Dracma e o Dracma não guarda papel no seu tenant. ### Erros e casos de borda | Mensagem ou código | Causa | O que fazer | |---|---|---| | "Não é possível remover o último administrador do tenant." (`last_admin`) | o pedido deixaria o tenant sem administrador | conceda o pacote a outra pessoa antes | | "Quem pediu não pode revisar o próprio item." (`self_review_forbidden`) | você votou no pedido que criou | peça a outro administrador | | "Você já votou neste item." (`already_voted`) | voto duplicado | nada a fazer | | "Este item já foi decidido." (`invalid_state_transition`) | outra pessoa decidiu antes | recarregue | | "Confirme com o fator forte para concluir." (`step_up_required`) | falta a confirmação recente | use **Confirmar identidade agora** | | "Confira os campos do formulário." (`validation_failed`) | e-mail inválido, ID que não é UUID, pacote fora da lista ou motivo curto | corrija | | Pessoa já tem o pacote / não tem o pacote | concessão repetida ou remoção de algo que não existe | confira a tabela | Tela sem acesso: quem ainda não tem pacote em nenhuma operação vê "Você ainda não tem acesso a nenhuma operação. Peça ao administrador do seu tenant." ### Veja também - [Conta, login e dispositivos](conta-e-login.md) - [O painel](README.md) - [Chaves de API](chaves-de-api.md) - [O que preparar](../onboarding/pre-requisitos.md) --- # Arquivo: docs/painel/tenants-e-operacoes.md ## Tenants e operações > Para: Adm — Conferido em 2026-10-18. Um **tenant** é a sua empresa no Dracma e uma **operação** é cada marca, país e ambiente que você integra. Nesta tela você cria operações de **Teste** (saem na hora) e de **Real** (dependem da aprovação de outra pessoa), acompanha os pedidos e revela a primeira chave. Tenants novos são criados pela equipe do Dracma. ### O que é - **Tenant:** o espaço da sua empresa. Reúne as operações e as pessoas que as administram. Quem gere os tenants do Dracma vê todos; você vê os tenants de que é dono. - **Operação:** uma integração completa, com as suas chaves, regras, webhook e saldos. Tem um **tipo**: - **Teste:** simulador, chave `key_test_…`, nenhum dinheiro se move; - **Real:** gateway de verdade, chave `key_live_…`, dinheiro de verdade. - **Nome (slug):** de 2 a 63 caracteres, só letras minúsculas, números e hífen, começando por letra ou número. O nome de tenant e o de operação são **únicos em todo o Dracma**, não só dentro do seu tenant. Uma convenção que evita colisão: `--`. Situação da operação, como a tela mostra: | Situação | Significa | |---|---| | **Configurando** | A operação foi criada, mas algum passo de provisionamento ainda não terminou (veja abaixo). | | **Ativa** | Todos os passos terminaram. | | **Suspensa** | A operação está bloqueada pela equipe do Dracma. A API responde `403` com `operation_suspended`. | Situação de um pedido de operação Real: | Situação | Significa | |---|---| | **Aguardando aprovação** | Ninguém decidiu ainda. | | **Aprovado e criado** | Foi aprovado e a operação existe. | | **Recusado** | Uma pessoa da equipe do Dracma recusou. | | **Cancelado** | Quem pediu desistiu. | | **Falhou ao criar** | Foi aprovado, mas a criação falhou; nada foi criado. Peça um novo pedido ou fale com o suporte. | ### Quando usar Use **Tenants e operações** para criar uma operação nova: outra marca, outro país, ou a operação Real depois de homologar a integração em Teste. Quem pode criar: - o **dono do tenant** (o primeiro Administrador, que aceitou o convite do Dracma); - a **equipe do Dracma**, que também cria tenants. Os demais papéis não veem o botão **Nova operação**. O item do menu **Tenants e operações** só aparece para quem é dono de algum tenant ou integra a equipe do Dracma. ### Como fazer #### Primeiro acesso: o convite ao administrador O primeiro administrador do tenant entra por **convite**, nunca por cadastro aberto. !!! note "Confirmar após deploy" O convite do administrador está em implantação. Os passos abaixo descrevem o fluxo definido no contrato; confirme com o suporte do Dracma, no onboarding, se ele já está ativo na sua instância. ```mermaid sequenceDiagram participant D as Equipe do Dracma participant P as Painel participant A as Administrador do operador participant I as Login do Dracma D->>P: Cria o tenant e informa o e-mail do administrador P->>I: Pede o convite I-->>A: E-mail com o link do convite (72 h, uso único) A->>I: Abre o link, entra com 2 fatores e aceita I-->>P: Convite aceito Note over A,P: A pessoa passa a ser dona do tenant A->>P: Cria a operação Teste e pede a chave ``` 1. A equipe do Dracma cria o tenant e informa o e-mail do seu administrador. O Dracma **não** cria a conta nem recebe a senha dele. 2. O administrador recebe um e-mail com um link. O link vale **72 horas** e **só pode ser usado uma vez**. Se vencer ou for perdido, a equipe do Dracma reenvia (até 5 vezes, com pelo menos 60 segundos entre os reenvios); cada reenvio invalida o link anterior. 3. Ele abre o link, entra (ou cria a conta) com **pelo menos dois fatores** e aceita o convite. O e-mail do convite precisa ser o e-mail verificado da conta. 4. Ao aceitar, ele vira **dono do tenant** e passa a ver **Tenants e operações**. 5. Pessoas da equipe do Dracma não aceitam o convite do operador, e quem criou o convite não pode aceitá-lo. !!! note "Confirmar após deploy" Na homologação o convite também chega a um e-mail de verdade. Informe o endereço real do administrador, não um endereço de teste. Depois que o tenant tem dono ativo, a equipe do Dracma **não** cria mais operações de Teste nele (a resposta é `tenant_has_owner`): é o dono quem cria e quem recebe a chave. #### Criar uma operação de Teste 1. Abra **Tenants e operações** e clique em **Nova operação**. 2. Escolha o tenant e o **nome** da operação. 3. Em **Tipo**, deixe **Teste** ("Simulador, chave `key_test_`. Nenhum dinheiro real. Sai na hora"). 4. Preencha os **IPs liberados** (um por linha) e, se já tiver, a **URL do webhook**. 5. Se a confirmação de identidade estiver vencida, use **Confirmar identidade agora** antes de enviar. 6. Clique em **Criar operação**. A operação aparece como **Configurando** e o painel mostra o aviso **Operação criada** com o botão **Revelar chave**. Os campos do formulário: | Campo | Regra | |---|---| | Tenant | Só aparecem os tenants em que você pode criar operação. | | Nome da operação | Slug de 2 a 63 caracteres, único no Dracma. | | Tipo | `Teste` ou `Real`. As opções dependem da instância (veja abaixo). | | IPs liberados | Pelo menos um e no máximo 50, um por linha. IPv4 com máscara de `/24` a `/32` e IPv6 de `/56` a `/128`; `/0` não é aceito. IP sem máscara vale como `/32` ou `/128`. | | URL do webhook | Opcional. `https://`, sem usuário e senha, até 512 caracteres, com domínio público: `localhost`, endereço IP privado ou de loopback e nomes sem ponto são recusados. | | Motivo | Só para Real: de 10 a 1000 caracteres. | !!! warning "Não disponível" A lista de IPs fica registrada, mas hoje o Dracma **não a aplica** às chamadas da API: uma chamada de um IP fora da lista não é recusada por isso. Veja [IPs do conector](allowlist-de-ip.md). Tipos permitidos por instância: | Instância | Teste | Real | |---|---|---| | Desenvolvimento | sim | não | | Homologação | sim | só se o ambiente estiver habilitado para isso; senão a criação é recusada com `environment_not_allowed` | | Produção | não | sim | Em produção, portanto, **não existe operação de Teste**: o Teste do operador vive na homologação. Veja [Ambientes, tipos de operação e hosts](../conceitos/ambientes-e-hosts.md#matriz-instancia-tipo). Fora de produção, o endereço do webhook também passa pela política de destinos de teste. Não use endereços de túnel: peça no onboarding a liberação do domínio do seu receptor. Veja [Seu endpoint](../webhooks/endpoint-e-politica-de-url.md#hosts-permitidos-fora-de-producao). #### Criar uma operação Real A operação Real passa por um **pedido** com a aprovação de outra pessoa. ```mermaid stateDiagram-v2 [*] --> Aguardando: Você pede (motivo + confirmação) Aguardando --> Criado: Uma pessoa da equipe do Dracma aprova Aguardando --> Recusado: Uma pessoa da equipe do Dracma recusa Aguardando --> Cancelado: Quem pediu cancela Criado --> Falhou: A criação falha Criado --> [*]: Quem pediu revela a chave ``` 1. No formulário, escolha **Real**, escreva o **motivo** (de 10 a 1000 caracteres: para que serve a operação) e preencha IPs e webhook como no Teste. 2. Clique em **Criar operação**. O pedido entra em **Aguardando aprovação** e aparece na tabela do tenant com essa situação e em **Seus pedidos**. 3. Uma pessoa da equipe do Dracma abre **Tenants e operações**, vê o pedido em **Aguardando a sua aprovação** e escolhe **Aprovar** ou **Recusar**, com um motivo opcional. Ela precisa de confirmação recente. 4. Aprovado, a operação é criada na hora e o pedido passa a **Aprovado e criado**. Não há espera de 24 h neste pedido. 5. Em **Seus pedidos**, **você** (quem pediu) clica em **Revelar chave**. Regras do pedido: - **Quem pediu nunca aprova o próprio pedido.** A tela só mostra os botões de voto para outra pessoa. - **Só quem pediu cancela**, e só enquanto estiver **Aguardando aprovação** (botão **Cancelar pedido**). - Um voto de recusa encerra o pedido. - Para pedir uma nova operação depois de uma recusa, abra outro pedido. #### O que a operação ganha ao ser criada Ao ser criada, a operação recebe: - moeda **BRL** com o método **PIX**; - a lista de IPs que você informou; - a primeira chave de API, com os quatro escopos: **Criar depósitos**, **Pedir saques**, **Ler dados** e **Propor mudança de regra**; - o **conjunto de regras inicial** do Dracma, já em vigor (não é preciso aprová-lo). Qualquer mudança depois dele é por pedido, como descrito em [Regras, listas e emergência](regras-e-listas.md); - o vínculo de permissões das pessoas da operação; - o endereço de webhook, se você informou a URL (sem aprovação nem espera de 24 h; mudar a URL depois passa por pedido, veja [Webhooks no painel](webhooks.md)). Os três últimos itens são feitos em segundo plano. Até terminarem, a operação aparece como **Configurando**. Se um passo falha (um serviço fora do ar, por exemplo), o Dracma tenta de novo sozinho, com esperas crescentes de 5 segundos até 10 minutos, e a operação não é perdida. !!! note "Exigências do gateway" Os dados do jogador que o gateway de depósito exige (`deposit_requirements` em `GET /v1/currencies`) são declarados pelo Dracma junto com a configuração do gateway, não pelo formulário da operação. Numa operação criada só pelo painel, a lista pode vir vazia até o Dracma declará-la, e a coluna **Gateway de entrada** mostra `—`. Se um depósito for recusado com `required_by_operation`, o erro diz qual campo faltou; peça ao suporte para conferir as exigências da operação. Veja [Ping e moedas](../referencia/ping-e-moedas.md#method_details-e-as-exigencias-por-operacao). Espere a situação **Ativa** antes de integrar. Se a operação continuar em **Configurando** por mais de alguns minutos, algum passo está falhando: acione o suporte com o nome da operação. #### Revelar a primeira chave - **Teste:** o aviso **Operação criada** traz o botão **Revelar chave**. - **Real:** o botão está em **Seus pedidos**, na linha do pedido **Aprovado e criado**. A chave (`key_test_…` ou `key_live_…`, mais o segredo que começa com `sk_test_` ou `sk_live_`) é mostrada **uma única vez**, só para quem criou a operação (Teste) ou fez o pedido (Real), em até **24 horas** e com confirmação recente. Copie para um cofre antes de sair da tela: ela não é exibida de novo. Se a janela vencer (`reveal_expired`) ou a chave já tiver sido mostrada (`already_revealed`), peça outra chave em [Chaves de API](chaves-de-api.md). #### Editar, apagar ou suspender !!! warning "Não disponível" O painel não edita nem apaga tenants e operações, e **não tem botão "Suspender operação"**. Para mudar o nome, apagar ou suspender uma operação, peça ao suporte do Dracma. Uma operação suspensa pela equipe do Dracma aparece como **Suspensa** e a API responde `403` com `operation_suspended` em todas as rotas. O botão de contenção que você tem no painel é **Suspender automático**, que desliga a aprovação automática de saques de uma moeda. Veja [Regras, listas e emergência](regras-e-listas.md). Para parar o uso de uma chave, [revogue a chave](chaves-de-api.md). ### Exemplos Formulário de uma operação de Teste, com dados fictícios: | Campo | Valor | |---|---| | Tenant | `acme` | | Nome da operação | `acme-br-hml` | | Tipo | Teste | | IPs liberados | `203.0.113.10` e `198.51.100.0/24` | | URL do webhook | `https://webhooks.acme.example/dracma` | A rede mínima da lista é `/24` em IPv4 e `/56` em IPv6: um bloco maior, como `203.0.113.0/16`, é recusado com "IP inválido". Mensagens que o formulário pode mostrar: | Mensagem | Causa | |---|---| | "Use de 2 a 63 caracteres: letras minúsculas, números e hífen, começando por letra ou número." | Nome fora do padrão. | | "Informe ao menos um IP liberado." | Campo de IPs vazio. | | "IP inválido. Use um por linha, IPv4 ou IPv6; com máscara, a rede mínima é /24 (IPv4) ou /56 (IPv6)." | Endereço mal formado, máscara menor que o mínimo ou mais de 50 itens. | | "A URL do webhook deve começar com https:// e não pode ter usuário ou senha." | URL com `http://` ou com credencial. | | "Escreva o motivo com 10 a 1000 caracteres (obrigatório para operação real)." | Real sem motivo. | ### Erros e casos de borda | Mensagem | Causa | O que fazer | |---|---|---| | "Já existe um tenant com esse nome." | O nome do tenant está em uso. | Escolha outro nome. | | "Já existe uma operação com esse nome neste tenant." | O nome da operação está em uso (a unicidade vale para todo o Dracma, apesar do texto). | Escolha outro nome. | | "Este tipo de operação não é permitido neste ambiente." | Teste em produção, ou Real numa instância sem esse tipo habilitado (`environment_not_allowed`). | Crie o tipo certo para a instância. | | "Quem pediu não pode aprovar o próprio pedido." | Você tentou votar no seu pedido (`self_review_forbidden`). | Peça a outra pessoa. | | "Você já votou neste pedido." | Voto repetido (`already_voted`). | Nenhuma. | | "Confirme com o fator forte para concluir." | Confirmação recente vencida (`step_up_required`). | Use **Confirmar identidade agora** e repita. | | "A chave já foi mostrada e não pode ser mostrada de novo." | `already_revealed`. | Peça outra chave. | | "O prazo para revelar a chave venceu." | `reveal_expired`. | Peça outra chave. | | "Sem permissão para esta ação." | Seu papel não permite (`forbidden`). | Peça a um Administrador. | | "Tenant, operação ou pedido não encontrado." | Item de outro tenant ou inexistente (`not_found`). | Confira o tenant. | | "Serviço indisponível agora. Tente de novo em instantes." | Dependência fora do ar (`service_unavailable`). | Repita em instantes. | | "Executor indisponível agora" | O serviço de gateways não respondeu. | Repita em instantes. | Casos de borda: - **Duas pessoas, um pedido.** Com uma só pessoa na equipe do Dracma que pode aprovar, quem pediu não consegue aprovar; peça que outra pessoa dessa equipe esteja disponível. - **Pedido aprovado, operação não criada.** O pedido fica **Falhou ao criar** e nada é criado. Abra um novo pedido; se repetir, acione o suporte. - **Operação em "Configurando" para sempre.** Um passo repete a falha. O motivo mais comum é um endereço de webhook recusado fora de produção. Veja [Seu endpoint](../webhooks/endpoint-e-politica-de-url.md#hosts-permitidos-fora-de-producao) e acione o suporte. - **Convite vencido.** Peça o reenvio à equipe do Dracma (veja o fluxo do convite acima). ### Veja também - [Chaves de API](chaves-de-api.md) - [IPs do conector](allowlist-de-ip.md) - [Webhooks no painel](webhooks.md) - [Gateways (só leitura)](gateways-e-psp.md) - [Equipe e papéis](equipe-e-papeis.md) - [De Teste para Real](../onboarding/promocao-para-producao.md) --- # Arquivo: docs/painel/chaves-de-api.md ## Chaves de API > Para: Adm · Dev — Conferido em 2026-10-18. Na tela **Chaves e IPs** o Administrador do tenant pede chaves novas, revela o segredo uma única vez, acompanha o uso de cada chave e revoga as que não servem mais. Pedir uma chave exige a aprovação de outra pessoa e uma espera de 24 horas; revogar é imediato. ### O que é Uma chave é um par de credenciais da **operação**: o identificador público (`key_test_…` em operação Teste, `key_live_…` em operação Real) e o **segredo** (`sk_test_…` ou `sk_live_…`), que assina cada chamada HMAC. Veja [Autenticação HMAC](../api/autenticacao.md). A chave de um tipo não vale no outro: chave `key_live_` numa operação de Teste, ou o inverso, recebe `401` com `unknown_key`. Cada chave leva um ou mais **escopos**, que limitam o que ela faz. Uma chamada sem o escopo da rota recebe `403` com `insufficient_scope`. | Rótulo na tela | Escopo | Permite | |---|---|---| | Criar depósitos | `deposits:write` | `POST /v1/deposits` e cancelamento de depósito | | Pedir saques | `withdrawals:write` | `POST /v1/withdrawals` e cancelamento de saque | | Ler dados | `read` | consultas e listagens (marcado por padrão ao pedir uma chave) | | Propor mudança de regra | `rules:propose` | `POST /v1/rules/change-requests` | Quem usa a tela: só o **Administrador do tenant** abre **Chaves e IPs**. Os outros papéis recebem "Não encontrado". O limite é de **2 chaves ativas por operação**; o título da seção mostra "Chaves de API (N de 2 ativas)". !!! warning "Não disponível" Os certificados mTLS aparecem numa seção da tela com o aviso "Emissão ainda não disponível": o Dracma não emite certificados de cliente hoje. A autenticação da sua operação é a assinatura HMAC. Veja [Rede, allowlist e mTLS](../seguranca/rede-allowlist-e-mtls.md#mtls). ### Quando usar - Para dar a um componente novo (conector, job de reconciliação) uma chave própria, com o menor escopo possível. - Para **rotacionar** uma chave, sem parar a integração. - Para **revogar** uma chave que vazou ou que não é mais usada. - Para decidir o que revogar: a tabela mostra o último uso e as recusas de cada chave. Escopos mínimos por componente (recomendação): o conector de depósitos usa `deposits:write` e `read`; o de saques usa `withdrawals:write` e `read`; um job só de conferência usa apenas `read`. ### Como fazer #### Pedir uma chave nova 1. Em **Criar chave de API**, marque os escopos (de 1 a 4, sem repetir). 2. Escreva o **motivo** (de 10 a 1000 caracteres). 3. Clique em **Pedir chave**. Se a confirmação de identidade estiver vencida, use **Confirmar identidade agora** antes. 4. O pedido aparece em **Pedidos em andamento** como **Aguardando aprovação**. 5. **Outro Administrador** abre o pedido, escreve o motivo da decisão e clica em **Aprovar** (ou **Recusar**). Quem pediu não aprova o próprio pedido. 6. Aprovado, o pedido passa a **Aprovado, no time-lock** e mostra "Xh XXmin para valer". A chave só nasce **24 horas depois da aprovação**. 7. Quando a espera termina, o Dracma cria a chave e a linha dela aparece na tabela como **Ativa, segredo ainda não revelado**. Se a operação já tem 2 chaves ativas, o botão **Pedir chave** fica desabilitado e a tela avisa: revogue uma antes. !!! warning "A aprovação é só metade da espera" "Aprovação de outra pessoa" significa uma pessoa além de quem pediu, **e** a espera de 24 horas. Planeje chaves novas com pelo menos um dia de antecedência. Durante a espera, qualquer Administrador pode **Cancelar pedido**. ```mermaid stateDiagram-v2 [*] --> Aguardando: Pedido (motivo + confirmação) Aguardando --> NoTimeLock: 1 aprovação de outra pessoa Aguardando --> Recusado: 1 recusa Aguardando --> Cancelado: Cancelar pedido NoTimeLock --> Cancelado: Cancelar pedido NoTimeLock --> ChaveCriada: 24 h depois da aprovação ChaveCriada --> Revelada: Quem pediu revela (até 24 h) ChaveCriada --> Revogada: Prazo vence sem revelar Revelada --> Revogada: Revogar ``` #### Revelar o segredo - O botão **Revelar segredo (uma vez)** aparece **só para quem pediu a chave**, na linha dela. Para as outras pessoas, a linha mostra "Segredo aguardando quem pediu a chave (até …)". - Revelar exige confirmação recente. O segredo aparece na tela com o aviso **Copie o segredo agora**; recarregar a página o apaga e o Dracma não o mostra de novo. - A janela para revelar é de **24 horas a partir do momento em que a chave nasce** (não do pedido). Passado o prazo, o Dracma **revoga a chave sem que o segredo tenha sido mostrado** e libera a vaga. - O escopo da chave não muda quem revela: vale para chave de leitura e para chave de escrita. A **primeira** chave de uma operação segue outro caminho: ela nasce junto com a operação e é revelada pelo botão **Revelar chave** da tela [Tenants e operações](tenants-e-operacoes.md), também uma vez e em até 24 horas. #### Rotacionar sem parar a integração A rotação é **manual**. Com o limite de 2 chaves ativas, o roteiro é: 1. Com a chave antiga ainda ativa, **peça a chave nova** (aprovação + 24 h). 2. Quando a chave nova nascer, **revele** o segredo e guarde no cofre. 3. Coloque a chave nova no conector e implante. 4. Confira na tabela: a chave nova mostra **Último uso**; a antiga deve parar de aparecer em uso. 5. **Revogue a chave antiga.** !!! warning "Não disponível" O Dracma **não expira chaves sozinho nem avisa** que uma chave está velha. A recomendação de trocar a chave a cada 90 dias é uma política sua: a tela não tem alerta de idade (o tipo de alerta de vencimento existe só para certificados, que também não são emitidos hoje). Anote a data de criação da chave e agende a rotação. #### Revogar uma chave 1. Na linha da chave, escreva o **motivo da revogação** (de 10 a 1000 caracteres). 2. Clique em **Revogar agora**. É preciso confirmação recente. **Uma pessoa basta**, sem espera. 3. A tela confirma: "Chave revogada. Deixa de valer em até 30 segundos em todas as réplicas." Depois disso, toda chamada com a chave recebe `401` com `unknown_key`. A revogação não tem volta; para voltar a usar, peça uma chave nova. Você pode revogar a última chave ativa, mas a integração fica sem credencial até a próxima nascer (24 horas). #### Perdi o segredo Não há como recuperar um segredo: o Dracma não o guarda em claro. Se a janela venceu, se a página foi recarregada antes de copiar ou se o segredo vazou: 1. Se a operação tem 2 chaves ativas, **revogue** a que perdeu o segredo (ou uma que não usa). 2. **Peça uma chave nova**, com outro ciclo de aprovação e 24 horas. 3. Atualize o conector. Respostas da tela para segredo indisponível: "Não há segredo a revelar: já foi mostrado, a janela venceu ou o pedido não é seu." #### Uso por chave A tabela de chaves mostra, para cada uma: | Coluna | O que mostra | |---|---| | Chave | O `key_id` e a data de criação. | | Escopos | Os rótulos dos escopos. | | Estado | `Ativa`, `Ativa, segredo ainda não revelado` ou `Revogada em …`. | | Último uso | Data e hora (horário de Brasília) e o IP de origem, ou "Sem uso registrado". | | Recusas em 24 h | Quantas chamadas com a chave receberam `401` e quantas receberam `403` (IP fora da lista). | O último uso e as recusas vêm de um registro **por amostragem**: use-os para decidir se uma chave ainda é usada, não como contagem exata. Se uma chave ativa acumula recusas `403`, a tela mostra o banner **Chave usada de IP fora da lista**, com a contagem e o último IP. Com a lista de IPs hoje não aplicada (veja [IPs do conector](allowlist-de-ip.md)), esse contador tende a ficar em zero. ### Exemplos Rotação planejada para uma terça-feira, com dois Administradores (Ana e Bruno), todos os horários de Brasília: | Quando | Quem | O que acontece | |---|---|---| | Seg 10:00 | Ana | Pede uma chave com `deposits:write` e `read`, motivo "Rotação trimestral do conector de depósitos". | | Seg 10:20 | Bruno | Aprova. O pedido mostra "23 h 59 min para valer". | | Ter 10:20 | Dracma | A espera termina e a chave nasce. A janela de 24 horas para revelar abre. | | Ter 11:00 | Ana | Revela o segredo, guarda no cofre e implanta o conector com a chave nova. | | Ter 15:00 | Ana | Vê o "Último uso" da chave nova e a antiga sem uso novo. | | Ter 15:10 | Ana | Revoga a chave antiga, motivo "Substituída pela chave de ter 10:20". | ### Erros e casos de borda | Mensagem | Causa | O que fazer | |---|---|---| | "Pedido de chave enviado. Precisa da aprovação de outra pessoa e do time-lock de 24 h." | Confirmação do pedido. | Peça a outro Administrador que aprove. | | "A operação já tem 2 chaves ativas. Revogue uma antes de criar outra." | `key_limit_reached`. Também pode aparecer ao fim da espera: o pedido vira **Falhou**. | Revogue uma chave e peça de novo. | | "Escolha ao menos um escopo da lista." | Nenhum escopo marcado. | Marque de 1 a 4. | | "Escreva um motivo de 10 a 1000 caracteres." | Motivo curto ou longo. | Ajuste. | | "Quem pediu não pode aprovar o próprio pedido." | `self_review_forbidden`. | Peça a outro Administrador. | | "Você já votou neste pedido." | `already_voted`. | Nenhuma. | | "Este item já foi decidido ou revogado." | `invalid_state_transition`: o pedido já terminou ou a chave já foi revogada. | Recarregue a página. | | "Confirme com o fator forte para concluir." | `step_up_required`. | Use **Confirmar identidade agora**. | | "Não há segredo a revelar: já foi mostrado, a janela venceu ou o pedido não é seu." | Segredo já revelado, janela vencida ou outra pessoa pediu. | Peça uma chave nova. | | "Item não encontrado nesta operação." | Chave de outra operação ou inexistente. | Confira a operação no seletor. | | "Sem permissão para esta ação." | Seu papel não é Administrador do tenant. | Peça a um Administrador. | Casos de borda: - **Só um Administrador no tenant.** Ninguém pode aprovar o pedido: inclua uma segunda pessoa em [Equipe e papéis](equipe-e-papeis.md) antes de precisar de uma chave. - **Pedido "Falhou".** O histórico recente mostra o motivo no fim da linha. O mais comum é o limite de 2 chaves atingido durante a espera. - **Chave nova sem uso.** Se, depois do deploy, a chave nova não aparece com "Último uso", o conector ainda usa a antiga ou está recebendo `401`. Confira o relógio do servidor e a assinatura em [Autenticação HMAC](../api/autenticacao.md). ### Veja também - [IPs do conector](allowlist-de-ip.md) - [Webhooks no painel](webhooks.md) - [Tenants e operações](tenants-e-operacoes.md) - [Autenticação HMAC](../api/autenticacao.md) - [Segredos e vazamento](../seguranca/segredos-e-credenciais.md) --- # Arquivo: docs/painel/allowlist-de-ip.md ## IPs do conector > Para: Adm — Conferido em 2026-10-18. A seção **Lista de IPs permitidos** da tela **Chaves e IPs** registra de onde o conector da operação chama a API. Incluir um IP exige a aprovação de outra pessoa e uma espera de 24 horas; remover é imediato. Hoje a lista fica registrada, mas o Dracma não a aplica às chamadas. ### O que é Cada operação tem uma lista de endereços IP e redes (CIDR) de onde o seu conector faz chamadas. A tela mostra, para cada entrada, a rede (`IP / CIDR`), o **rótulo**, quem incluiu e quando. Entradas criadas junto com a operação aparecem como incluídas pelo provisionamento. !!! warning "Não disponível" O Dracma **não aplica a lista de IPs às chamadas** hoje, em nenhum ambiente: uma chamada com assinatura válida de um IP que não está na lista **não** recebe `403 ip_not_allowed`, e o aviso "Chave usada de IP fora da lista" não é gerado. A lista continua sendo registrada e mantida. Alguns textos da tela ainda dizem que, sem IP na lista, nenhuma chamada passa; isso não vale enquanto a lista não é aplicada. A autenticação da chamada é a assinatura HMAC. Veja [Rede, allowlist e mTLS](../seguranca/rede-allowlist-e-mtls.md#lista-de-ips-de-origem). Quem usa a tela: só o **Administrador do tenant**. A seção fica na mesma página das [Chaves de API](chaves-de-api.md). ### Quando usar Mantenha a lista com os endereços de saída reais do conector (o NAT do cluster, o IP fixo do servidor). Ela é o seu registro de onde a integração deve chamar, e a [tabela de chaves](chaves-de-api.md) mostra o último IP de cada chave para você comparar. Inclua um IP novo **antes** de mudar a rede do conector (novo provedor, novo cluster, ambiente de recuperação), contando com pelo menos 24 horas mais o tempo de uma segunda pessoa aprovar. ### Como fazer #### Incluir um IP 1. Em **Incluir IP na lista**, informe o **IP ou CIDR** (por exemplo `203.0.113.0/24`). 2. Dê um **rótulo** de 1 a 80 caracteres (por exemplo "cluster principal"). 3. Escreva o **motivo** (de 10 a 1000 caracteres). 4. Clique em **Pedir inclusão**. É preciso confirmação recente de identidade. 5. O pedido aparece em **Pedidos em andamento**. **Outro Administrador** o aprova com um motivo. 6. Depois da aprovação, o pedido fica **Aprovado, no time-lock** e a entrada entra na lista **24 horas depois**. Durante a espera, qualquer Administrador pode cancelar. É o mesmo fluxo do pedido de chave nova: veja os detalhes em [Chaves de API](chaves-de-api.md). #### Formato aceito A lista aceita apenas redes **pequenas**: o tamanho mínimo da rede é `/24` em IPv4 e `/56` em IPv6. Em outras palavras, o número depois da barra tem de ser **igual ou maior** que o mínimo: | Tipo | Aceito | Recusado | |---|---|---| | IPv4 | `/24` a `/32` (de 256 endereços a 1) | `/23` ou menor número (rede maior) e `/0` | | IPv6 | `/56` a `/128` | `/55` ou menor número | | IP sem máscara | vale como `/32` (IPv4) ou `/128` (IPv6) | — | Outras regras: - O endereço deve ser o **início da rede**: `203.0.113.5/24` é recusado; use `203.0.113.0/24`. - Não há máscara com zero à esquerda nem endereço com identificador de zona. - Uma entrada repetida é recusada com "Este IP já está na lista." #### Remover um IP 1. Na linha da entrada, escreva o **motivo para remover** (de 10 a 1000 caracteres). 2. Clique em **Remover agora**. É preciso confirmação recente. **Uma pessoa basta** e vale na hora. 3. Se a lista ficar vazia, a tela avisa: "IP removido. A lista ficou vazia". Incluir e remover **não são simétricos**: remover é restritivo e imediato; incluir amplia a lista e por isso passa por aprovação e espera. #### Diagnóstico de 403 Enquanto a lista não é aplicada, uma chamada recusada não vem de IP fora da lista. Se a lista voltar a ser aplicada, o roteiro é: 1. Abra **Chaves e IPs** e veja o banner **Chave usada de IP fora da lista**: ele traz a chave, quantas recusas houve nas últimas 24 horas e o último IP. 2. Se o IP é do seu conector, peça a inclusão (aprovação + 24 horas). Se não é, **revogue a chave**. 3. Na tabela, a coluna **Recusas em 24 h** separa os `401` dos `403`. ### Exemplos Todos os endereços são de documentação. | Entrada | Resultado | |---|---| | `203.0.113.10` | Aceita, vira `203.0.113.10/32`. | | `198.51.100.0/24` | Aceita. | | `198.51.100.0/22` | Recusada: a rede é maior que `/24`. | | `198.51.100.7/24` | Recusada: o endereço não é o início da rede. | | `2001:db8:abcd::/56` | Aceita. | | `2001:db8::/32` | Recusada: a rede é maior que `/56`. | ### Erros e casos de borda | Mensagem | Causa | O que fazer | |---|---|---| | "IP ou CIDR inválido (IPv4 de /24 a /32, IPv6 de /56 a /128)." | Formato fora do aceito. | Corrija conforme a tabela acima. | | "O rótulo precisa ter de 1 a 80 caracteres." | Rótulo vazio ou longo. | Ajuste. | | "Este IP já está na lista." | A entrada já existe (`already_exists`). | Nenhuma. | | "Pedido de IP enviado. Precisa da aprovação de outra pessoa e do time-lock de 24 h." | Confirmação do pedido. | Peça aprovação a outro Administrador. | | "Quem pediu não pode aprovar o próprio pedido." | `self_review_forbidden`. | Peça a outro Administrador. | | "Confirme com o fator forte para concluir." | `step_up_required`. | Use **Confirmar identidade agora**. | | "Este item já foi decidido ou revogado." | O pedido já terminou ou a entrada já foi removida. | Recarregue a página. | Casos de borda: - **Lista vazia.** Hoje não bloqueia nenhuma chamada. O aviso da tela sobre "nenhuma chamada passa" não vale enquanto a lista não é aplicada. - **Entrada duplicada no meio da espera.** Se a mesma rede for incluída por outro pedido durante as 24 horas, o segundo pedido termina como **Falhou**, sem efeito. - **Várias redes.** Inclua uma entrada por pedido. Na criação da operação o formulário aceita até 50 entradas de uma vez. ### Veja também - [Chaves de API](chaves-de-api.md) - [Rede, allowlist e mTLS](../seguranca/rede-allowlist-e-mtls.md) - [Tenants e operações](tenants-e-operacoes.md) - [Autenticação HMAC](../api/autenticacao.md) --- # Arquivo: docs/painel/webhooks.md ## Webhooks no painel > Para: Adm · Dev — Conferido em 2026-10-18. A tela **Webhooks** é onde você configura o endereço que recebe os eventos da operação, gira o segredo de assinatura, acompanha as entregas e trata os eventos que esgotaram as tentativas. Ela tem quatro abas: **Configuração**, **Entregas**, **DLQ** e **Saúde**. O formato dos eventos e a verificação da assinatura estão na seção [Webhooks](../webhooks/README.md). ### O que é A configuração de webhook **é feita só pelo painel**: não existe API pública para cadastrar ou alterar a URL, os eventos ou o segredo. Cada operação tem **um** endereço de webhook. Quem vê e quem gere: | Papel | Vê as abas | Muda URL, eventos, segredo e DLQ | |---|---|---| | Administrador do tenant | sim | sim | | Financeiro | sim | não | | Auditor | sim | não | | Revisor | não (a tela responde "Não encontrado") | não | Quem só vê não enxerga o formulário de proposta, o botão de rotação nem a seleção da DLQ. As quatro abas: | Aba | Para que serve | |---|---| | **Configuração** | URL, eventos assinados, situação do segredo, pedidos de alteração. | | **Entregas** | Uma linha por evento enviado, com filtros e detalhe das tentativas. | | **DLQ** | Eventos que esgotaram as tentativas. Prévia, reentrega e "marcar como resolvido". | | **Saúde** | Taxa de resposta `2xx` por hora nas últimas 24 horas. | ### Quando usar - No go-live de cada operação: informar a URL, escolher os eventos e gerar o primeiro segredo. - Quando o receptor muda de endereço ou de ambiente. - Para investigar por que um evento não chegou: **Entregas** e **Saúde**. - Depois de uma indisponibilidade do receptor: **DLQ**. ### Como fazer #### Aba Configuração A aba mostra: - **Destino:** a URL cadastrada (ou "Não configurada"), o ambiente (`Sandbox` ou `Produção`) e a marca **HTTPS obrigatório** nas operações Reais. - **Eventos assinados:** a lista dos tipos escolhidos, ou "Todos os eventos" quando nenhum foi marcado. - **Segredo de assinatura:** a situação (`Nenhum segredo`, `Ativo` ou `Em rotação (duas assinaturas v1=)`) e a tabela de segredos vigentes com identificador, data de criação, data de expiração e estado. **O valor do segredo nunca é exibido.** - **Pedidos de alteração** (só para o Administrador): os pedidos recentes, com votos e a espera restante. O catálogo de eventos que você pode assinar tem 17 tipos: | Grupo | Tipos | |---|---| | Depósito | `deposit.pending`, `deposit.under_review`, `deposit.paid`, `deposit.expired`, `deposit.cancelled`, `deposit.failed`, `deposit.returned`, `deposit.refunded`, `deposit.chargeback` | | Saque | `withdrawal.under_review`, `withdrawal.approved`, `withdrawal.processing`, `withdrawal.paid`, `withdrawal.rejected`, `withdrawal.failed`, `withdrawal.cancelled`, `withdrawal.returned` | Veja o conteúdo de cada um em [Catálogo de eventos](../webhooks/catalogo-de-eventos.md). #### Mudar a URL ou os eventos ```mermaid stateDiagram-v2 [*] --> Simulacao: Propor alteração (motivo + confirmação) Simulacao --> Aguardando: URL e eventos aprovados na checagem Simulacao --> [*]: Recusado na checagem Aguardando --> Aprovado: 1 aprovação de outra pessoa Aguardando --> Recusado: 1 recusa Aguardando --> Cancelado: Cancelar pedido Aprovado --> Cancelado: Cancelar pedido Aprovado --> Vigente: 24 h depois da aprovação ``` 1. Na aba **Configuração**, em **Propor alteração**, informe a **URL de destino** (até 2048 caracteres) e marque os **tipos de evento**. Sem nenhum marcado, a operação recebe todos. 2. Escreva o **motivo** (de 10 a 1000 caracteres) e clique em **Propor alteração**. É preciso confirmação recente. 3. Antes de abrir o pedido, o Dracma **simula** a alteração: valida o formato da URL e a política de destino (a mesma que vale nas entregas). Uma URL recusada nessa simulação não gera pedido. Veja [Seu endpoint](../webhooks/endpoint-e-politica-de-url.md). 4. **Outro Administrador** aprova ou recusa o pedido, com motivo. Quem propôs não vota. 5. Aprovado, o pedido fica **Aprovado, no time-lock** e a alteração só vigora **24 horas depois**. Qualquer Administrador pode cancelar nesse intervalo. Regras da URL: - A URL deve ser `https://`. Em operação **Real** isso é obrigatório e a tela mostra a marca **HTTPS obrigatório**. - Sem usuário e senha na URL e sem trecho `#`. - Os eventos precisam estar no catálogo, sem repetição. O **primeiro** endereço pode entrar de duas formas: - informado no formulário de criação da operação: o Dracma o cadastra em segundo plano, **sem pedido e sem espera** (veja [Tenants e operações](tenants-e-operacoes.md)); - proposto nesta aba, quando a operação nasceu sem URL: segue o fluxo acima, com **aprovação e 24 horas**. Planeje o go-live com esse prazo. !!! note "Domínio fora de produção" Fora de produção, os endereços de entrega são limitados a domínios liberados. Peça a liberação do domínio do seu receptor no onboarding. Endereços temporários de túnel não funcionam. #### Girar o segredo - O botão **Rotacionar segredo** só aparece quando a operação **já tem URL** cadastrada. O **primeiro** segredo só nasce por esse botão: cadastrar a URL não cria segredo. - Uma pessoa basta, com confirmação recente. Não há aprovação nem espera. - O segredo novo aparece **uma vez**, num campo para copiar, com o identificador e "sobreposição de 7 dias". Recarregar a tela o apaga. - Durante 7 dias a entrega leva **duas** assinaturas `v1=`: a do segredo antigo e a do novo. Troque o segredo no receptor nesse período. - Não existe "revogar o segredo antigo" no painel. Uma segunda rotação encerra a sobreposição. Veja [Segredo e rotação](../webhooks/segredos-e-rotacao.md#segredo-vazado-revogar-e-rotacionar-duas-vezes). #### Aba Entregas A aba lista as entregas da operação, da mais recente para a mais antiga (50 por vez). Filtros: - **Status:** `Pendente`, `Entregue`, `DLQ` ou `Ignorado`. Quando uma entrega é ignorada, o motivo aparece abaixo do status. - **Tipo de evento:** qualquer um dos 17 do catálogo. Colunas: o evento (clique para abrir o detalhe), o tipo, o status, o número de tentativas, o último código HTTP, a latência da última tentativa e a próxima tentativa. O **detalhe** de uma entrega mostra o evento, o status e a tabela de tentativas em ordem: número, horário (Brasília), código HTTP, latência em milissegundos e o erro, quando houve. O último erro tem no máximo 200 caracteres. #### Aba DLQ A **DLQ** guarda os eventos que esgotaram as tentativas de entrega (72 horas sem resposta `2xx`). **Nenhum evento é perdido**: ele continua disponível para reentrega e em `GET /v1/events`. A aba mostra até 100 eventos, incluindo os já resolvidos, com o evento, o tipo, o número de tentativas, o último HTTP e a situação (`Na DLQ` ou `Resolvido em …` com o motivo). Reentregar: 1. Marque os eventos (até **50 por vez**). 2. Clique em **Prévia**. A tela mostra o **destino** (URL e ambiente) e os eventos; nada é enviado ainda. Eventos que não estão na DLQ aparecem em "Não encontrados na DLQ". 3. Clique em **Reentregar**. Cada evento é reenviado com o **mesmo** `Dracma-Event-Id`, com uma nova janela de 72 horas, e o resultado vem por evento: `Reagendado`, `Não encontrado` ou `Falhou`. Marcar como resolvido: 1. Marque os eventos e escreva o **motivo** (de 10 a 1000 caracteres): por exemplo, "tratado manualmente pelo ticket 123". 2. Clique em **Marcar como resolvido**. O evento sai da fila de pendentes e fica registrado com o motivo. Reentregar e resolver não pedem aprovação nem confirmação recente; ficam na [Auditoria](auditoria.md) com o autor. #### Aba Saúde A aba mostra uma tabela com as **últimas 24 horas**, hora a hora (horário de Brasília): tentativas, quantas deram `2xx` e a taxa. No alto: - **"Sem falha contínua na URL."** quando está tudo bem; - um alerta em vermelho quando a URL **falha há 15 minutos ou mais**, com a hora da última tentativa e do último sucesso. #### Testar a URL (só Teste) Na aba **Entregas** de uma operação de **Teste**, o Administrador vê **Testar a URL (sandbox)**: informe o identificador de um evento que já existe e o Dracma o reenvia para a sua URL, com o mesmo `Dracma-Event-Id`. Não existe um evento sintético de teste. Para testar, gere um depósito e simule o pagamento (veja [Cobrança de teste](cobranca-de-teste.md)) e use o evento resultante. !!! warning "Não disponível" Em operação **Real** não há teste de URL: a tela mostra "O teste de URL só existe em operações sandbox" e a resposta é `409` com `sandbox_only`. ### Exemplos Cronograma de uma mudança de URL, com dois Administradores (Ana e Bruno), horários de Brasília: | Quando | Quem | O que acontece | |---|---|---| | Qua 09:00 | Ana | Propõe `https://webhooks.acme.example/dracma` com todos os eventos, motivo "Novo receptor". A simulação aprova. | | Qua 09:15 | Bruno | Aprova, com o motivo "Conferi o domínio". O pedido mostra "23 h 59 min para valer". | | Qui 09:15 | Dracma | A alteração vigora. | | Qui 09:30 | Ana | Clica em **Rotacionar segredo**, copia o valor e o instala no receptor. | Depois de uma queda do receptor que gerou eventos na DLQ: 1. Na aba **Saúde**, confirme que a URL voltou a responder. 2. Na aba **DLQ**, marque os eventos, clique em **Prévia** e depois em **Reentregar**. 3. Confira o resultado `Reagendado` de cada evento e acompanhe em **Entregas**. ### Erros e casos de borda | Mensagem | Causa | O que fazer | |---|---|---| | "Proposta registrada. Ela só vigora após aprovação de outra pessoa e 24 h de espera." | Confirmação do pedido. | Peça a outro Administrador que aprove. | | "URL inválida: use https, sem usuário e senha na URL." | URL fora do formato. | Corrija. | | "Evento ou tipo de evento inválido." / "Selecione ao menos um evento." | Evento fora do catálogo, ou nenhum marcado na DLQ. | Corrija. | | "Selecione no máximo 50 eventos por vez." | Lote da DLQ maior que 50. | Divida em lotes. | | "A api recusou os dados enviados. Confira os campos." | A simulação recusou a URL ou o motivo. | Veja a política de URL em [Seu endpoint](../webhooks/endpoint-e-politica-de-url.md). | | "Escreva um motivo de 10 a 1000 caracteres." | Motivo curto ou longo. | Ajuste. | | "Quem propôs não pode aprovar o próprio pedido." | `self_review_forbidden`. | Peça a outro Administrador. | | "Você já votou neste pedido." | `already_voted`. | Nenhuma. | | "O item mudou de estado e a ação não se aplica mais." | O pedido já terminou. | Recarregue a página. | | "Confirme com o fator forte (\"Confirmar identidade agora\") e tente de novo." | `step_up_required`. | Use o link e repita. | | "O serviço de webhooks está indisponível agora. Tente de novo em instantes." | O serviço de entrega não respondeu. | Repita em instantes; o que já foi pedido não se perde. | | "O teste de URL só existe em operações sandbox." | `sandbox_only`. | Teste na operação de Teste. | Casos de borda: - **Segredo vazado.** O painel não revoga o segredo antigo na hora. Gire o segredo duas vezes seguidas para encerrar a sobreposição e confira os eventos com a busca em `GET /v1/events`, que não depende do webhook. Veja [Segredos e vazamento](../seguranca/segredos-e-credenciais.md#procedimento-de-emergencia-um-segredo-vazou). - **Lista de IPs de saída do Dracma.** Não disponível: o Dracma não publica essa lista, no painel nem em outro lugar, e não garante IP fixo de origem. Não filtre a rota do webhook por IP; veja [Seu endpoint](../webhooks/endpoint-e-politica-de-url.md#ips-de-saida-do-dracma). - **Operação nova sem segredo.** É esperado: o primeiro segredo só existe depois de **Rotacionar segredo**. ### Veja também - [Webhooks](../webhooks/README.md) - [Receptor passo a passo](../webhooks/receptor.md) - [Retentativas, skipped e DLQ](../webhooks/retentativas-e-dlq.md) - [Alertas e e-mails](alertas.md) - [Chaves de API](chaves-de-api.md) --- # Arquivo: docs/painel/gateways-e-psp.md ## Gateways (só leitura) > Para: Adm — Conferido em 2026-10-18. A tela **Gateways** mostra, em modo de leitura, as conexões de PSP (o provedor de pagamento) da operação: qual é, para que direção serve, em que ordem é usada e se está ativa. O cadastro e a ativação de uma conexão são feitos pela equipe do Dracma. Você não digita nem vê credenciais. ### O que é Uma **conexão** liga a operação a um PSP para uma direção: **Entrada** (receber depósitos), **Saída** (pagar saques) ou **Entrada e saída**. A operação pode ter mais de uma conexão. A tela (menu **Plataforma**, item **Gateways**) lista, para a operação escolhida no seletor: | Coluna | O que mostra | |---|---| | PSP | O nome do provedor, como `MaximaxPay` ou `Mercado Pago`. | | Direção | `Entrada`, `Saída` ou `Entrada e saída`. | | Prioridade | Um número de 1 a 1000; **menor número = tentado primeiro**. | | Situação | `Ativa` ou `Inativa`. | | Credencial cadastrada | `Sim` ou `Não`. É só o indicador: o valor nunca aparece. | | Criada em | Data e hora, no horário de Brasília. | Qualquer pessoa com acesso à operação vê a tela. Uma operação de **Teste** sem conexão usa o simulador do Dracma; nesse caso a tela mostra "Nenhuma conexão cadastrada nesta operação", e isso é o esperado. ### Quando usar Consulte a tela para: - confirmar, antes do go-live, que a operação Real tem uma conexão **Ativa** para entrada e uma para saída (veja [De Teste para Real](../onboarding/promocao-para-producao.md)); - conferir a ordem das conexões quando houver mais de uma; - entender por que um depósito ou saque não anda: conexão `Inativa` ou sem credencial. Qual PSP atende cada tipo de operação e cada direção está em [PSPs](../dinheiro/psps-e-ambientes.md). ### Como funciona **Quem cadastra.** Só a equipe do Dracma cadastra uma conexão. Você não tem botão para isso, e as credenciais do PSP nunca passam pelo painel do operador. A tela diz: "As credenciais são cadastradas pelo suporte do Dracma; o painel nunca as mostra." **Como a conexão fica ativa.** ```mermaid sequenceDiagram participant S1 as Equipe do Dracma (1) participant S2 as Equipe do Dracma (2) participant PSP as PSP participant O as Você S1->>S1: Cadastra a conexão (nasce Inativa) S1->>PSP: Registra a URL de callback da conexão S1->>S2: Propõe a ativação (prioridade e direção) S2->>S1: Aprova (outra pessoa, com confirmação) Note over S1,S2: A conexão passa a Ativa O->>O: Vê a linha como Ativa em Gateways ``` 1. A conexão nasce **Inativa**, com prioridade entre 1 e 1000. 2. A equipe do Dracma registra no PSP a **URL de callback** da conexão (veja abaixo). 3. A ativação é um **pedido** com aprovação de **outra pessoa** da equipe do Dracma e confirmação recente de identidade. Não há espera de 24 horas. 4. Mudar prioridade, direção ou desativar uma conexão segue o mesmo caminho. Desativar uma conexão pode afetar cobranças e saques em andamento nela, por isso a equipe do Dracma prefere mudar a prioridade. Você acompanha só o resultado: a linha passa de `Inativa` para `Ativa`. !!! warning "Não disponível" O painel do operador não permite cadastrar, ativar, desativar nem reordenar conexões. Peça ao suporte do Dracma. O mesmo vale para trocar as credenciais do PSP. ### Callback não é webhook São dois canais diferentes, em direções opostas: | | Callback | Webhook | |---|---|---| | Quem chama | O PSP | O Dracma | | Quem recebe | O Dracma, no host `cb.` | O seu receptor | | Quem configura | A equipe do Dracma, no painel do PSP | Você, em [Webhooks no painel](webhooks.md) | | O que informa | Que um pagamento ou saque mudou de estado no PSP | Que um depósito ou saque mudou de estado no Dracma | A URL de callback é própria de cada conexão e só a equipe do Dracma a usa. Você não precisa registrar nem proteger o callback; o que você recebe do Dracma são os webhooks. ### Conta de PSP compartilhada Uma mesma conta de PSP do Dracma pode atender mais de uma operação. A gestão dessas contas é só da equipe do Dracma e não aparece para você. O efeito para o seu lado é que **saldo e reconciliação são sempre da sua operação**, no livro do Dracma, e nunca o extrato do PSP. A tela [Saldos e caixa](saldos-e-caixa.md) deixa isso explícito. ### Exemplos Operação Real pronta para o go-live: | PSP | Direção | Prioridade | Situação | Credencial cadastrada | |---|---|---|---|---| | `MaximaxPay` | Entrada e saída | 1 | Ativa | Sim | Operação Real que ainda **não** está pronta: | PSP | Direção | Prioridade | Situação | Credencial cadastrada | |---|---|---|---|---| | `Mercado Pago` | Entrada | 1 | Inativa | Sim | Nesse caso, aguarde a ativação pela equipe do Dracma antes de gerar depósitos reais. ### Erros e casos de borda - **"Nenhuma conexão cadastrada nesta operação."** Normal em operação de Teste (usa o simulador). Em operação Real, falta o cadastro: acione o suporte. - **Conexão `Ativa` com "Credencial cadastrada: Não".** Não deveria acontecer; acione o suporte. - **Tela em branco ou "Não encontrado".** Você abriu a tela sem escolher uma operação a que tem acesso. Escolha a operação no seletor. - **"Executor indisponível agora".** O serviço que lê as conexões não respondeu. Repita em instantes. - **Prioridade.** A ordem da tela não significa que o Dracma troque sozinho de conexão quando uma falha: veja em [PSPs](../dinheiro/psps-e-ambientes.md) como a escolha da conexão funciona. ### Veja também - [PSPs](../dinheiro/psps-e-ambientes.md) - [Webhooks no painel](webhooks.md) - [Saldos e caixa](saldos-e-caixa.md) - [De Teste para Real](../onboarding/promocao-para-producao.md) --- # Arquivo: docs/painel/transacoes.md ## Transações > Para: Fin — Conferido em 2026-10-18. A tela **Transações** é o extrato da operação: uma lista com os depósitos e saques, do mais recente para o mais antigo, com a pessoa identificada só pela referência que o seu sistema enviou. Dados pessoais não aparecem nesta tela. ### O que é O extrato lê os depósitos e saques da operação escolhida no seletor, pelos mesmos dados da API. Cada linha tem: | Coluna | O que mostra | |---|---| | Quando | Data e hora de criação, no horário de Brasília. | | Tipo | `Depósito` ou `Saque`. | | Id | O identificador público: `dep_…` para depósito, `wdr_…` para saque. É o mesmo `id` que a API devolve. | | Jogador | A **referência externa** do jogador (`external_ref`), exatamente como o seu sistema enviou. | | Situação | O estado atual, como `pending`, `under_review`, `approved`, `processing`, `paid`, `rejected`, `failed`, `cancelled` ou `returned`. | | Valor | O valor em reais (`R$ 1.234,56`). | Quem vê: todas as pessoas com acesso à operação (Administrador do tenant, Financeiro, Revisor e Auditor). ### Quando usar Use o extrato para: - achar um depósito ou saque pelo horário, pelo valor ou pela referência do jogador; - conferir o estado atual de um item que o suporte ou o seu time citou; - ver o movimento recente sem abrir a API. Para filtrar por período, estado ou moeda, ou para baixar os dados, use [Relatórios](relatorios.md). Para fechar o dia contra o seu livro, use [Reconciliação no painel](reconciliacao.md). ### Como funciona 1. Abra **Transações** no menu **Operação**. Se a operação não estiver escolhida, o painel abre a última usada. 2. Use os botões **Todas**, **Depósitos** e **Saques** para filtrar o tipo. 3. A lista mostra 50 itens por página. Clique em **Mais antigas** para continuar. O extrato **nunca mostra dados pessoais**: nem nome, nem documento, nem chave PIX, nem e-mail. O que identifica a pessoa é o `external_ref`, a referência que o seu sistema escolheu ao criar o depósito ou o saque. Em outras telas os dados aparecem **mascarados** (CPF `***.456.789-**`, por exemplo). Para ver o valor completo: - em um item **em revisão**, use **Revelar** no detalhe, com motivo e confirmação de identidade (veja [Revisão](revisao-de-depositos-e-saques.md)); - em um relatório, peça a exportação com dados pessoais, que exige a aprovação de outra pessoa (veja [Relatórios](relatorios.md)). Toda revelação fica na [Auditoria](auditoria.md). ### Exemplos Trecho de um extrato, com dados fictícios: | Quando | Tipo | Id | Jogador | Situação | Valor | |---|---|---|---|---|---| | 18/10/2026 14:32 | Saque | `wdr_0123456789abcdef0123456789abcdef` | `jogador-0042` | `under_review` | R$ 1.500,00 | | 18/10/2026 14:10 | Depósito | `dep_89abcdef0123456789abcdef01234567` | `jogador-0042` | `paid` | R$ 200,00 | | 18/10/2026 13:58 | Depósito | `dep_fedcba9876543210fedcba9876543210` | `jogador-0107` | `pending` | R$ 50,00 | Para entender cada estado, veja [Ciclo do depósito](../dinheiro/ciclo-do-deposito.md) e [Ciclo do saque](../dinheiro/ciclo-do-saque.md). ### Erros e casos de borda - **"Nenhuma transação."** A operação ainda não tem movimento, ou o filtro de tipo não encontra itens. - **"Não encontrado."** Você não tem acesso à operação, ou o endereço não leva a uma operação sua. - **A lista não tem busca nem filtro por período.** Exporte um relatório de transações com os filtros de período, estado, método, moeda e tipo. - **O valor parece diferente do que você enviou.** A coluna **Valor** mostra o valor do pedido (`amount`). Se o valor pago de um depósito for diferente, ele está em `paid_amount` na API. - **Estado desconhecido.** O extrato mostra o estado como ele vem do Dracma. Estados que o seu sistema não conhece devem ser tratados como não finais, como descrito em [Estados no seu produto](../conector/estados-no-produto.md). ### Veja também - [Revisão](revisao-de-depositos-e-saques.md) - [Relatórios](relatorios.md) - [Reconciliação no painel](reconciliacao.md) - [Depósitos](../referencia/depositos.md) - [Saques](../referencia/saques.md) --- # Arquivo: docs/painel/cobranca-de-teste.md ## Cobrança de teste > Para: Adm · Fin — Conferido em 2026-10-18. Na tela **Cobrança de teste** você gera um depósito PIX da operação pelo próprio painel, sem escrever código, e acompanha o QR, o estado, os eventos e as entregas de webhook. Em operação de Teste a cobrança é simulada e você pode simular o pagamento. Em operação Real a cobrança é de verdade e pode ser paga. ### O que é A tela cria um **depósito PIX** pelo mesmo serviço que atende `POST /v1/deposits`. Por isso a cobrança segue as mesmas regras, passa pela decisão (pode ir para revisão) e gera os mesmos eventos. O que a diferencia: - o `external_id` do depósito começa com `painel-teste-`; - o jogador recebe uma referência `painel-teste-…`, derivada do documento informado (o mesmo documento gera sempre a mesma referência); - a lista mostra as **últimas 50 cobranças** criadas pela tela. Quem usa: **Administrador do tenant** e **Financeiro**. Revisor e Auditor recebem "Não encontrado". | | Operação de Teste | Operação Real | |---|---|---| | Aviso da tela | "Operação sandbox: a cobrança é simulada e você poderá simular o pagamento." | "Operação de PRODUÇÃO: a cobrança gerada aqui é REAL e pode ser paga de verdade." | | Dinheiro | Nenhum se move. | Move dinheiro real. | | Simular pagamento | Disponível. | Não existe. | | Confirmação extra | Não. | Caixa **"Entendo que esta cobrança é REAL e gera um PIX de verdade"**. | | Teto de valor | Nenhum. | Nenhum. | !!! danger "Dinheiro real" Numa operação Real, o QR gerado aqui é um PIX válido. Se alguém o pagar, o dinheiro entra no caixa da operação e vira um depósito pago de verdade. Use valores mínimos e pague com uma conta sua. ### Quando usar - Para ver um depósito andar de ponta a ponta antes de ter o conector pronto. - Para gerar um evento real e testar o seu receptor de webhook (veja [Webhooks no painel](webhooks.md), aba **Entregas**). - No go-live: a **primeira transação real de valor mínimo** pode ser feita aqui, numa operação Real (veja [De Teste para Real](../onboarding/promocao-para-producao.md)). ### Como fazer #### Gerar a cobrança 1. Em **Nova cobrança**, preencha: - **Valor (R$):** maior que zero, com até 2 casas. Aceita `10`, `10,5`, `1.234,56` e `10.50`. - **Nome do pagador:** de 2 a 120 caracteres. - **CPF ou CNPJ do pagador:** com ou sem máscara; os dígitos verificadores são conferidos. - **E-mail do pagador:** opcional. Algumas operações o exigem. - **Motivo:** de 10 a 1000 caracteres. 2. Em operação Real, marque a confirmação de que a cobrança é real. 3. Clique em **Gerar cobrança**. É preciso confirmação recente de identidade. 4. O painel abre o detalhe da cobrança. Fora da instância de produção, o e-mail do pagador precisa usar um domínio reservado (`.test`, `.invalid` ou `.example`); caso contrário a tela mostra "e-mail do pagador: use um e-mail de domínio de teste". Para gerar documentos de teste válidos, veja [Dados de teste](../sandbox/dados-de-teste.md#cpf-de-teste). #### Acompanhar e simular o pagamento O detalhe mostra: - **Resumo:** estado, valor, valor pago (se houver), ambiente (`Sandbox (simulado)` ou `Produção (REAL)`), referência, criação, expiração e pagamento. - **Pagamento PIX:** o QR e o código copia e cola, com botão de copiar, enquanto o depósito não terminou. - **Linha do tempo:** os eventos `deposit.*` do depósito, do mais antigo para o mais novo. - **Entregas de webhook** desses eventos: o resultado de cada envio para a sua URL. A página se atualiza sozinha a cada 3 segundos, com a aba visível, até o estado ficar final (`paid`, `expired`, `cancelled`, `failed`, `returned`, `refunded` ou `chargeback`). Você pode desmarcar **Atualizar sozinho a cada 3 s** ou usar **Atualizar agora**. Em operação de Teste, enquanto o depósito não terminou, aparece o botão **Simular pagamento**. Ele exige confirmação recente e leva o depósito a `paid`, como se o pagador tivesse pago. O painel confirma com "Pagamento simulado. O status atualiza sozinho." !!! warning "Não disponível" Simular o pagamento só existe em operação de Teste. Em operação Real o botão não aparece e a API responde `409` com `sandbox_only`. Numa operação Real, o único jeito de concluir a cobrança é pagar o PIX. Estados que a tela mostra: | Estado | Rótulo | |---|---| | `pending` | Aguardando pagamento | | `under_review` | Em revisão | | `paid` | Pago | | `expired` | Expirado | | `cancelled` | Cancelado | | `failed` | Falhou | | `returned` | Devolvido | | `refunded` | Reembolsado | | `chargeback` | Chargeback | Um depósito `Em revisão` aparece na fila da [Revisão](revisao-de-depositos-e-saques.md). O ciclo completo está em [Ciclo do depósito](../dinheiro/ciclo-do-deposito.md). #### O que o seu sistema recebe A cobrança de teste é um depósito de verdade para o Dracma: o webhook da operação recebe os eventos `deposit.*` dela, com `external_id` começando por `painel-teste-`. O seu sistema não criou esse depósito, então o receptor deve **ignorar com segurança** eventos de `external_id` desconhecido e responder `2xx`, sem tratar como erro. Veja [Receptor passo a passo](../webhooks/receptor.md). ### Exemplos Cobrança de teste com dados fictícios: | Campo | Valor | |---|---| | Valor (R$) | `10,50` | | Nome do pagador | `Pessoa de Teste` | | CPF ou CNPJ | um CPF de teste válido | | E-mail do pagador | `pagador@exemplo.test` | | Motivo | `Validar o receptor de webhook do ambiente de homologação` | Roteiro de um teste do receptor: 1. Gere a cobrança. 2. Clique em **Simular pagamento**. 3. Na linha do tempo, veja `deposit.pending` e `deposit.paid`. 4. Em **Entregas de webhook**, confirme o código `2xx` de cada um. ### Erros e casos de borda | Mensagem | Causa | O que fazer | |---|---|---| | "Informe um valor maior que zero, em reais, com até 2 casas decimais (ex.: 10,50)." | Valor inválido (`valor_invalido`). | Corrija. | | "Informe o nome do pagador (2 a 120 caracteres)." | Nome curto ou longo. | Corrija. | | "CPF ou CNPJ inválido. Confira os dígitos." | Documento mal formado ou com dígito verificador errado. | Use um documento válido. | | "E-mail inválido." | Formato do e-mail. | Corrija. | | "A operação exige dados adicionais do pagador (por exemplo e-mail). Confira os campos." | A operação exige um campo que a tela não preencheu (`requisitos`). | Preencha o que a lista abaixo da mensagem pedir. | | "Escreva um motivo de 10 a 1000 caracteres." | Motivo curto ou longo. | Corrija. | | "confirmação de cobrança real: obrigatório" | Falta a caixa de confirmação numa operação Real. | Marque a caixa. | | "Confirme com o fator forte (\"Confirmar identidade agora\") e tente de novo." | `step_up_required`. | Use o link e repita. | | "Simular pagamento só existe em operações sandbox." | `sandbox_only`. | Use a operação de Teste. | | "A cobrança mudou de estado e a ação não se aplica mais." | O depósito já terminou. | Recarregue. | | "Cobrança não encontrada nesta operação." | Identificador de outra operação ou inexistente. | Volte à lista. | | "O serviço está indisponível agora. Tente de novo em instantes." | Dependência fora do ar. | Repita em instantes. | Casos de borda: - **A tela exige dados que ela não tem.** Se a operação exigir o IP ou o identificador de dispositivo do jogador, a tela avisa "a tela não envia este dado; ajuste a exigência da operação" e não consegue gerar a cobrança. Use a API para esse caso. - **Cobrança em revisão ou recusada pelas regras.** É um depósito normal: acompanhe em [Revisão](revisao-de-depositos-e-saques.md) ou em [Transações](transacoes.md). - **Entregas "indisponíveis".** Se o serviço de webhooks estiver fora, o bloco de entregas fica vazio, mas o resto da tela segue funcionando. ### Veja também - [Webhooks no painel](webhooks.md) - [Revisão](revisao-de-depositos-e-saques.md) - [Primeiro depósito e primeiro saque](../onboarding/quickstart-sandbox.md) - [Depósitos](../referencia/depositos.md) --- # Arquivo: docs/painel/revisao-de-depositos-e-saques.md ## Revisão > Para: Fin — Conferido em 2026-10-18. As telas **Saques em revisão** e **Depósitos em revisão** listam os itens que as regras não liberaram sozinhas. Cada item precisa da decisão de uma ou duas pessoas, com motivo e confirmação de identidade. Esta página explica a fila, o detalhe, a regra de votos, a revelação de dados pessoais e o pedido de informação. ### O que é Quando as regras da operação mandam um saque ou um depósito para **revisão** (`under_review`), o item aparece na fila. O painel mostra só os itens nesse estado. Quem vê e decide: **Financeiro** e **Revisor**. Administrador e Auditor recebem "Não encontrado" nessas telas. **Fila.** Uma linha por item, com: | Coluna | O que mostra | |---|---| | Saque / Depósito | O `external_id` (clique para abrir) e a data. | | Valor | Em reais. | | Jogador | A referência externa e o documento **mascarado**. | | Motivos | Um selo por motivo de revisão. | | Votos | Quantos votos há e quantos são necessários (`0/1`, `1/2`). | | Prazo | O limite de decisão. | O **prazo** é de **24 horas** a partir da criação do item. Passado o prazo, o alerta "SLA de revisão estourado" pode disparar (veja [Alertas e e-mails](alertas.md)). Num saque, um pedido de informação pausa o relógio. **Motivos mais comuns** (o catálogo completo está em [Decisão, limites e regras](../dinheiro/decisao-limites-e-regras.md#catalogo-de-decisionreasons)): | Selo na tela | Código | |---|---| | Primeiro saque do jogador | `FIRST_WITHDRAWAL` | | Chave PIX nova | `PIX_KEY_CHANGED` | | Titularidade não verificada | `OWNERSHIP_UNVERIFIED` | | Destino usado por outro jogador | `SHARED_DESTINATION` | | Documento usado por outro jogador | `SHARED_DOCUMENT` | | Contexto novo (IP/dispositivo) | `NEW_CONTEXT` | | Exige aprovação dupla | `DUAL_APPROVAL_REQUIRED` | | Pagador compartilhado (PLD) | `AML_SHARED_PAYER` | Os outros códigos aparecem como vêm das regras. ### Quando usar Reserve um horário fixo por dia (e acompanhe o alerta de fila) para esvaziar a fila: um saque em revisão mantém o dinheiro reservado do jogador, e um depósito em revisão ainda não entrou no caixa. O botão **Saques em revisão** do **Início** mostra quantos itens esperam em cada operação. ### Como fazer #### Decidir um item 1. Abra o item. O detalhe traz **Dados** (valor, jogador, destino mascarado no saque, criação, prazo), **Por que está em revisão** (os motivos e a versão das regras) e **Votos**. 2. Em **Decidir**, escreva o **motivo da decisão** (de 10 a 1000 caracteres). 3. Clique no botão: - **Saque:** **Aprovar** ou **Recusar**; - **Depósito:** **Aceitar** ou **Devolver**. 4. Se a confirmação de identidade estiver vencida, a tela pede fator forte recente (**Confirmar identidade agora**) e volta ao mesmo item. O que cada decisão faz: | Item | Decisão | Resultado | |---|---|---| | Saque | Aprovar | O saque vai a `approved` e o Dracma o envia ao PSP quando o horário e o caixa permitem. Veja [Ciclo do saque](../dinheiro/ciclo-do-saque.md). | | Saque | Recusar | O saque vai a `rejected` e a reserva volta ao jogador. | | Depósito | Aceitar | O depósito vai a `paid`, pelo valor registrado. | | Depósito | Devolver | O depósito vai a `returned`. Veja [Ciclo do depósito](../dinheiro/ciclo-do-deposito.md). | Cada decisão emite os eventos correspondentes para o seu webhook. ```mermaid flowchart TD A[Item em revisão] --> B{Voto} B -- Recusar ou Devolver --> C[Decide na hora: rejected ou returned] B -- Aprovar ou Aceitar --> D{Exige aprovação dupla?} D -- Não --> E[1 pessoa basta: approved ou paid] D -- Sim --> F{2 pessoas diferentes?} F -- Ainda não --> A F -- Sim --> E ``` **Regra de votos.** - **Um voto contra decide.** Recusar ou Devolver encerra o item na hora, mesmo que outra pessoa já tenha aprovado. - **A favor:** basta **uma** pessoa, ou **duas pessoas diferentes** quando o item tem o motivo `DUAL_APPROVAL_REQUIRED`. Com uma equipe de duas pessoas, as duas precisam aprovar. - Cada pessoa vota **uma vez** por item. - Em um item cujo pedido de revisão foi registrado por uma pessoa, quem pediu não vota nele. **Aprovação dupla.** O motivo `DUAL_APPROVAL_REQUIRED` é gravado pelas regras da operação quando o valor do saque é igual ou maior que o limite de aprovação dupla (campo `dual_approval_above` do conjunto de regras, visível em `GET /v1/rules`) ou quando outra regra o exige. !!! warning "Não disponível" O painel **não cancela** saques nem depósitos: ele só decide itens em revisão. O cancelamento é feito pela API (veja [Saques](../referencia/saques.md) e [Depósitos](../referencia/depositos.md)). #### Revelar dados pessoais Na fila e no detalhe os dados vêm **mascarados** (CPF `***.456.789-**`, chave PIX parcial). Para ver o valor completo: 1. No cartão **Dados pessoais**, escreva o **motivo** (de 10 a 500 caracteres). 2. Clique em **Revelar**. É preciso confirmação recente. 3. A tela mostra os campos em claro: - **saque:** documento, nome, chave PIX e tipo da chave; - **depósito:** documento, nome do pagador e documento do pagador. O Dracma grava a revelação na [Auditoria](auditoria.md) **antes** de devolver os dados: sem registro, não há dado. Os dados em claro existem só nesta tela, não ficam guardados e somem ao sair ou recarregar. Quem revela: **Financeiro** e **Revisor**. #### Pedir informação (saques) Em um **saque** em revisão, o cartão **Informação solicitada ao operador** permite pedir esclarecimento ao time da operação sem decidir ainda. 1. Em **Pedir informação**, escreva o **texto para o operador** (de 10 a 1000 caracteres). **Não inclua dados pessoais do jogador**: o texto vai por e-mail. 2. Clique em **Pedir informação**. O Dracma registra o pedido e envia um e-mail aos **contatos de alerta da operação**. 3. O **relógio do prazo fica pausado** enquanto houver pedido aberto. O cartão mostra o **prazo original**, o **prazo efetivo** e a faixa "Relógio do SLA pausado desde …". 4. A resposta do operador é combinada fora do painel, pelo canal de suporte informado no onboarding. Quando chegar, clique em **Registrar resposta** (com um resumo opcional de até 1000 caracteres). O relógio volta a correr. O estado do aviso por e-mail aparece em cada pedido: | Texto na tela | Significa | |---|---| | Aviso enviado aos contatos do operador | O e-mail foi aceito para envio. | | Aviso pendente — será reenviado | O serviço de e-mail estava fora; o Dracma tenta de novo sozinho. | | Operação sem contatos de e-mail cadastrados | Não há contato de alerta na operação. | | Falha ao avisar | O envio falhou depois das tentativas. | Os contatos de alerta são definidos pelo Dracma no onboarding e não são editáveis no painel (veja [Alertas e e-mails](alertas.md)). Fora da produção esses e-mails não chegam a destinatários reais (ficam retidos num destino de teste), embora o painel mostre o aviso como enviado. Quem pede e registra: **Financeiro** e **Revisor**. Pedir ou responder não exige confirmação recente. ### Exemplos Um saque com dois motivos, numa operação com a equipe Ana (Financeiro) e Bruno (Revisor): | Passo | Quem | O que acontece | |---|---|---| | 1 | Sistema | Saque de R$ 3.000,00 com `FIRST_WITHDRAWAL` e `DUAL_APPROVAL_REQUIRED`. Votos `0/2`. | | 2 | Ana | Pede informação sobre a titularidade da chave PIX. O prazo fica pausado. | | 3 | Ana | Registra a resposta quando o time confirma. O prazo volta a correr. | | 4 | Ana | Aprova, motivo "Titularidade conferida pelo time da operação". Votos `1/2`. | | 5 | Bruno | Aprova, motivo "Segunda conferência". Votos `2/2`: o saque vai a `approved`. | Se, no passo 5, Bruno tivesse recusado, o saque iria a `rejected` na hora. ### Erros e casos de borda | Mensagem | Causa | O que fazer | |---|---|---| | "Voto registrado." | Sucesso. | Nenhuma. | | "Você já votou neste item." | `already_voted`. | Peça a outra pessoa. | | "Quem pediu não pode revisar o próprio item." | `self_review_forbidden`. | Peça a outra pessoa. | | "Este item já foi decidido." | `invalid_state_transition`: outra pessoa decidiu antes ou o estado mudou. | Recarregue a fila. | | "Escreva um motivo de 10 a 1000 caracteres." | Motivo curto ou longo. | Corrija. | | "Confirme com o fator forte para concluir." | `step_up_required`. | Use **Confirmar identidade agora**. | | "Item não encontrado nesta operação." | Item de outra operação ou já fora da revisão. | Volte à fila. | | "Sem permissão para esta ação." | Seu papel não decide. | Peça a um Financeiro ou Revisor. | | "Não foi possível concluir a ação." ao **Aceitar** um depósito | O depósito está em revisão por `AMOUNT_MISMATCH` (valor pago diferente): o aceite é proibido (`accept_not_allowed`). | Use **Devolver**. | | "Escreva o motivo (10 a 500 caracteres)." | Motivo da revelação fora do limite. | Corrija. | | "Revelar exige confirmação recente com fator forte" | `step_up_required` na revelação. | Use **Confirmar identidade agora**. | | "Este saque já foi decidido ou o pedido já foi respondido." | Pedido de informação num saque que saiu da revisão. | Nenhuma. | | "Escreva um texto de 10 a 1000 caracteres (a resposta aceita até 1000)." | Texto fora do limite. | Corrija. | Casos de borda: - **Equipe de uma pessoa.** Um item com `DUAL_APPROVAL_REQUIRED` nunca fecha. Inclua uma segunda pessoa em [Equipe e papéis](equipe-e-papeis.md). - **Duas decisões ao mesmo tempo.** Só uma vale. A segunda recebe "Este item já foi decidido". - **Item parado em `approved`.** Se o saque foi aprovado mas não saiu, veja [Ciclo do saque](../dinheiro/ciclo-do-saque.md#approved-parado) e [Saldos e caixa](saldos-e-caixa.md) (saques retidos). - **Prazo estourado.** O prazo é um limite de atendimento exibido na tela e usado pelo alerta; o painel não decide o item por você. ### Veja também - [Transações](transacoes.md) - [Saldos e caixa](saldos-e-caixa.md) - [Auditoria](auditoria.md) - [Regras, listas e emergência](regras-e-listas.md) - [Decisão, limites e regras](../dinheiro/decisao-limites-e-regras.md) --- # Arquivo: docs/painel/regras-e-listas.md ## Regras, listas e emergência > Para: Fin · Adm — Conferido em 2026-10-18. A área **Regras e listas** é onde você vê as regras que decidem se um saque ou depósito é automático, vai para revisão ou é bloqueado, propõe mudanças com simulação e aprovação, mantém listas de bloqueio e VIPs e aciona o botão de emergência. Ela tem seis abas: **Visão geral**, **Propor mudança**, **Listas**, **VIP**, **Calendário** e **Emergência**. ### O que é As **regras** são um conjunto versionado por operação: limites de depósito e saque, o teto do saque automático, o multiplicador do automático, a aprovação dupla, o horário do automático, o calendário de feriados e outras políticas. O mesmo conteúdo está em `GET /v1/rules` (veja [Regras (API)](../referencia/regras.md)); aqui você o vê e o altera pela tela, sem escrever código. !!! note "Valores" Esta página não lista os valores padrão das regras. Consulte `GET /v1/rules` ou a aba **Visão geral** para ver o que vale na sua operação. A lógica de decisão está em [Decisão, limites e regras](../dinheiro/decisao-limites-e-regras.md). Quem faz o quê: | Ação | Quem | |---|---| | Ver regras, histórico, pedidos, calendário e listas | Administrador do tenant, Financeiro, Auditor | | Propor mudança de regra, calendário ou reversão | Administrador do tenant, Financeiro | | Aprovar ou recusar mudança de regra | Financeiro | | Incluir, importar e remover entradas de lista; propor e revogar VIP | Financeiro | | Suspender o automático e confirmar a emergência | Administrador do tenant, Financeiro | Revisor não vê a área. O mesmo conjunto de regras pode ser proposto pela API com a chave de escopo `rules:propose`; as propostas feitas por ela aparecem na tela só para leitura. ### Quando usar - Para apertar ou afrouxar limites com segurança, vendo antes o efeito sobre os últimos 30 dias. - Para ajustar o horário do automático ou os feriados. - Para bloquear um documento, uma chave PIX, um IP ou um dispositivo, ou para dar a um jogador um limite próprio (VIP). - Para **parar o saque automático** de uma moeda na hora, em caso de suspeita de fraude. ### Como fazer #### Regras iniciais e mudanças Toda operação nasce com o **conjunto de regras inicial** do Dracma, **já em vigor**: ele não passa por aprovação. Daí em diante, qualquer mudança é um **pedido**: | Sentido | O que é | Aprovação | Espera | |---|---|---|---| | **Restritiva** | Torna as regras mais rígidas: limite menor, automático desligado, feriado a mais. | 1 pessoa além de quem propôs | nenhuma: vigora quando aprovada | | **Relaxante** | Torna as regras mais folgadas: limite maior, automático ampliado. | 1 pessoa além de quem propôs | 24 horas por padrão (o pedido mostra o valor vigente) | Uma proposta que mistura os dois sentidos conta como **relaxante**. Quem propõe **nunca** aprova a própria proposta. #### Propor uma mudança 1. Abra **Propor mudança**. A tela mostra os campos principais de cada moeda e os globais, além de um campo **JSON avançado** com o conjunto completo. 2. Edite os campos. Valores em dinheiro vão na **menor unidade** (`1000` = R$ 10,00). Campos que aceitam "desligado" ficam em branco. 3. Clique em **Simular/Validar**. O painel valida e mostra uma tabela **campo a campo** com o sentido de cada mudança (`Restritiva` ou `Relaxante`). 4. Se a mudança for relaxante, a tela mostra a **Simulação dos últimos 30 dias**: quantos saques cairiam em cada desfecho (`Automático`, `Revisão (1 aprovação)`, `Revisão (2 aprovações)`, `Bloqueado (422)`), os itens que **mudariam de revisão para automático** e a carga da fila por dia. 5. Escreva a **justificativa** (de 10 a 1000 caracteres). Na mudança relaxante, marque **Li o relatório da simulação**. 6. Clique em **Propor mudança**. É preciso confirmação recente. Campos que a tela edita, por moeda: depósito mínimo e máximo; saque mínimo e máximo; automático (máximo por saque e por dia); multiplicador do automático; máximo de saques por dia; aprovação dupla a partir de. Globais: automático ligado; primeiro saque em revisão; pagador deve ser o jogador; espera após trocar a chave PIX; política fora da janela do automático. !!! note "Limite da simulação" A simulação **não recalcula em cascata**: um saque que passaria a ser automático não altera o limite diário dos pedidos seguintes. Use o relatório como estimativa, não como previsão exata. O sentido é decidido pelo Dracma, campo a campo. Se a regra mudou desde que você abriu a tela, o envio é recusado com "A regra mudou desde que você abriu a tela. Recarregue a página e refaça a edição." **Reverter** uma versão antiga: no **Histórico de versões**, **Reverter para esta versão** abre uma proposta com o conteúdo daquela versão. Ela cria uma **nova** versão ao entrar em vigor e segue o mesmo fluxo. #### Aprovar, recusar e acompanhar Na **Visão geral**, **Pedidos de mudança pendentes** lista o pedido, o sentido, quem propôs, a justificativa, os votos (`x de N`) e o time-lock. Clique no pedido para ver o **Resumo**, a **Decisão**, **O que muda** (campo a campo) e as **Regras propostas**. - **Aprovar** e **Recusar** aparecem para o Financeiro que não propôs e ainda não votou. Pede motivo e confirmação recente. - **Cancelar pedido** aparece para quem pode propor ou aprovar, enquanto o pedido não vigorou. - Um voto de recusa encerra o pedido. Estados do pedido: `Aguardando aprovação`, `Aprovado, em time-lock`, `Em vigor`, `Recusado`, `Cancelado` e `Falhou ao aplicar`. Durante o time-lock a tela conta o tempo restante ("faltam 2 h 05 min") e, vencido, "vigora na próxima execução". #### Calendário e horário do automático A aba **Calendário** mostra o fuso da operação, a janela do automático, os dias da semana (1 = segunda a 7 = domingo), o que acontece fora da janela (`Aguardar a abertura` ou `Mandar para revisão`), o calendário nacional (`BR-national`) e as datas extras. Para propor mudança, use o formulário da aba: ligar o calendário nacional ou acrescentar uma data é **restritivo**; desligá-lo ou remover uma data é **relaxante**. O fluxo é o mesmo (Simular, justificar, propor). O fuso e o horário detalhados estão em [Dia, fuso e horários](../dinheiro/fuso-dia-e-horarios.md). #### Listas de bloqueio A aba **Listas** mostra as entradas ativas (situação, tipo, efeito, referência, motivo, quem criou, validade) e as remoções pendentes. **Tipos de entrada:** documento (CPF/CNPJ), referência do jogador (`external_ref`), destino (chave PIX, com o tipo `cpf`, `cnpj`, `email`, `phone` ou `random`), IP e dispositivo. **Efeito:** `Bloquear`, ou `Mandar para revisão` (só para IP e dispositivo). O valor digitado é guardado apenas como impressão digital; a tela mostra uma referência, nunca o valor. !!! note "Tipos aceitos" O seletor da tela pode listar tipos além dos acima. A API aceita só os cinco descritos; qualquer outro é recusado com `validation_failed`. - **Incluir entrada** vale **na hora**: 1 pessoa (Financeiro), com motivo e confirmação recente, sem aprovação. A tela avisa: "incluir vale na hora (efeito imediato, sem aprovação)". - **Importar CSV:** cabeçalho `kind,value,key_type,effect,reason,expires_at`, até **500 linhas** e **256 KiB**. A **prévia** mostra quantas linhas são válidas, inválidas e duplicadas, com os valores mascarados; só as válidas e novas são gravadas, e a importação também vale na hora. - **Remover entrada** é um **pedido**: 1 aprovação de outra pessoa e time-lock. A entrada continua valendo até o pedido vigorar. Bloquear é restritivo, por isso entra sem espera; tirar o bloqueio é relaxante, por isso espera. #### VIP Um **VIP** dá a um jogador, numa moeda, um limite próprio do automático: um **multiplicador** e/ou um **máximo por saque**. Para **propor**: 1. Informe a **referência do jogador** (`external_ref`), a moeda, ao menos um limite e a data **Vale até** (amanhã até **90 dias** a partir de hoje). 2. Escreva o motivo e clique em **Propor VIP**. É preciso confirmação recente. 3. O pedido precisa de **1 aprovação** de outra pessoa e do time-lock. Só então o VIP passa a valer. **Revogar** um VIP é restritivo: vale na hora, com motivo e confirmação recente. A aba mostra os VIPs (jogador, moeda, situação, validade) e os pedidos pendentes. #### Emergência: suspender o automático É o botão de contenção da operação. **Suspender automático** desliga o saque automático **de uma moeda** na hora (o automático e o multiplicador ficam zerados): os saques novos dessa moeda passam a ir para revisão. 1. Na aba **Emergência**, no cartão da moeda, digite o **código da moeda** (por exemplo `BRL`) em "Digite BRL para confirmar". Isso evita suspender a moeda errada. 2. Escreva o motivo (de 10 a 1000 caracteres) e clique em **Suspender automático**. É preciso confirmação recente. **Uma pessoa basta** para acionar. 3. **Outra pessoa** confirma em até **24 horas**, com motivo, em **Emergências registradas**. Quem acionou nunca vê o botão **Confirmar**. 4. Se passar de 24 horas sem confirmação, a tela mostra "ATENÇÃO: passou de 24 h sem a confirmação de uma segunda pessoa." A suspensão continua valendo. Para **religar** o automático use **Propor mudança**: é uma mudança relaxante (aprovação e espera). O alerta "Uso do interruptor de emergência" avisa a equipe (veja [Alertas e e-mails](alertas.md)). !!! warning "Não disponível" O painel não tem botão para suspender a **operação** inteira. O que existe é esta suspensão do automático por moeda. Para suspender a operação, peça ao suporte do Dracma (veja [Tenants e operações](tenants-e-operacoes.md)). ### Exemplos Apertar o automático, com a equipe Ana (Administrador) e Bruno (Financeiro): | Passo | Quem | O que acontece | |---|---|---| | 1 | Ana | Em **Propor mudança**, reduz o "Automático: máximo por saque" e clica em **Simular/Validar**. A tabela marca o campo como `Restritiva`. | | 2 | Ana | Escreve "Reduzir o teto do automático após aumento de fraude" e propõe. | | 3 | Bruno | Abre o pedido, confere **O que muda** e clica em **Aprovar**. | | 4 | Dracma | O pedido passa a **Em vigor** logo após a aprovação. | Se Ana quisesse **afrouxar** o mesmo campo, ela veria a simulação dos últimos 30 dias, marcaria **Li o relatório da simulação** e, depois da aprovação do Bruno, esperaria 24 horas. ### Erros e casos de borda | Mensagem | Causa | O que fazer | |---|---|---| | "A regra mudou desde que você abriu a tela. Recarregue a página e refaça a edição." | A regra vigente mudou (`etag_mismatch`). | Recarregue e refaça. | | "Marque que leu o relatório da simulação para propor um afrouxamento." | Falta a caixa na mudança relaxante. | Marque a caixa. | | "Nada mudou em relação à regra vigente." | A proposta é igual à regra atual. | Altere um campo. | | "O JSON das regras não é um objeto válido." | JSON avançado inválido. | Corrija o JSON. | | "Quem propôs (ou acionou) não pode aprovar, recusar ou confirmar o próprio pedido." | `self_review_forbidden`. | Peça a outra pessoa. | | "Você já votou neste pedido." | `already_voted`. | Nenhuma. | | "Este pedido já foi encerrado ou não aceita essa ação agora." | `invalid_state_transition`. | Recarregue. | | "A confirmação precisa ser igual ao código da moeda." / "Moeda inválida (3 letras maiúsculas)." | Emergência mal preenchida. | Digite o código exato. | | "A validade do VIP deve ser futura e de no máximo 90 dias." | Data do VIP fora da janela. | Ajuste a data. | | "Informe ao menos um limite do VIP (multiplicador ou máximo por saque)." | VIP sem limite. | Preencha um limite. | | "O CSV passa de 256 KiB." / "O CSV passa de 500 linhas." | Arquivo grande demais. | Divida o arquivo. | | "O CSV da confirmação foi alterado ou está corrompido. Refaça a prévia." | O conteúdo mudou entre a prévia e a confirmação. | Refaça a prévia. | | "Data inválida. Use o formato AAAA-MM-DD." | Data de expiração mal formada. | Corrija. | | "Confirme com o fator forte para concluir." | `step_up_required`. | Use **Confirmar identidade agora**. | | "Sem permissão para esta ação." | Seu papel não permite. | Peça a quem tem o papel. | Casos de borda: - **Equipe de uma pessoa.** Nenhuma mudança de regra, remoção de lista ou VIP fecha, porque falta quem aprove. A emergência e a inclusão em lista funcionam, mas a confirmação da emergência fica em atraso. - **Proposta feita pela API.** Aparece em "Propostas pela API pública (somente leitura)"; decida-a pelo fluxo da API. - **Relaxar logo depois de endurecer.** São dois pedidos independentes, cada um com a sua aprovação. ### Veja também - [Decisão, limites e regras](../dinheiro/decisao-limites-e-regras.md) - [Regras (API)](../referencia/regras.md) - [Revisão](revisao-de-depositos-e-saques.md) - [Alertas e e-mails](alertas.md) - [Auditoria](auditoria.md) --- # Arquivo: docs/painel/saldos-e-caixa.md ## Saldos e caixa > Para: Fin — Conferido em 2026-10-18. A tela **Saldos e caixa** mostra, por moeda, quanto há disponível, quanto está a receber e a pagar, quanto vai sair com os saques já aprovados ou em revisão, e quais saques aprovados ficaram retidos. Ela também guarda o limiar de alerta de caixa baixo. Os números são os do livro do Dracma, não os de uma conta no PSP. ### O que é Quem vê: **Administrador do tenant**, **Financeiro** e **Auditor**. Quem define o limiar de alerta: só o **Financeiro**. O Revisor não abre a tela. No alto aparece "Operação … · posição de" com a data e a hora da leitura (horário de Brasília). Cada moeda habilitada tem um cartão com cinco linhas: | Linha | O que é | |---|---| | **Disponível** | O saldo da moeda no livro do Dracma. **Inclui** o dinheiro já reservado para saques em andamento. | | **A receber** | Depósitos que ainda não foram liquidados no livro (`pending_in`). | | **A pagar** | O que já está reservado ou enviado para saques em andamento (`pending_out`). | | **Saída prevista** | A soma dos saques **aprovados** e **em revisão** que ainda não foram enviados ao PSP, com a contagem e o valor de cada grupo. | | **Disponível projetado** | `Disponível` menos `Saída prevista`. Pode ficar negativo. | As definições de cada saldo, e de **caixa livre**, estão em [Saldos e caixa livre](../dinheiro/saldos.md). ```mermaid flowchart LR D[Disponível
inclui o reservado] -->|menos A pagar| L[Caixa livre] L -->|menos Saída prevista| P[Caixa livre projetado] D -->|menos Saída prevista| T[Disponível projetado
da tela] ``` !!! note "Leitura conservadora" A tela calcula o **Disponível projetado** como `Disponível − Saída prevista`. Como o **Disponível** inclui o valor já reservado (**A pagar**), o caixa que de fato sustenta novos saques é `Disponível − A pagar`. Para uma conta conservadora, desconte também o **A pagar**: `Disponível − A pagar − Saída prevista`. A linha de ajuda "pode ser usado em saques", ao lado do **Disponível**, não considera o reservado. ### Quando usar - Todo dia, antes de liberar a fila de revisão: aprovar saques sem caixa deixa o dinheiro parado em `approved`. - Quando um saque não sai: veja se ele está em **Saques retidos aguardando despacho**. - Para configurar o aviso de caixa baixo. - No go-live, para conferir o caixa inicial da operação (veja [Checklist de go-live](../go-live/checklist.md#6-dinheiro-provas)). ### Como fazer #### Ler um cartão 1. Abra **Saldos e caixa** no menu **Operação**. 2. Em cada moeda, olhe os avisos em vermelho: - "A saída prevista passa do disponível: faltam R$ … de caixa." quando o **Disponível projetado** é negativo; - "Disponível abaixo do limiar de alerta (R$ …)." quando o **Disponível** é menor que o limiar definido. 3. Compare o **Disponível projetado** com o seu planejamento do dia e decida se vale aprovar mais saques. Uma moeda sem movimento aparece zerada. Se a operação não tem moeda habilitada, a tela mostra "Esta operação não tem moeda habilitada." #### Definir o limiar de alerta 1. No cartão da moeda, em **Limiar de alerta**, digite o valor mínimo desejado para o **Disponível** (por exemplo `10.000,00`). 2. Clique em **Definir limiar**. É preciso confirmação recente de identidade. 3. Para tirar o limiar, clique em **Remover limiar**. O limiar é **um valor por moeda** e vale ao mesmo tempo para o aviso da tela e para o alerta "Caixa abaixo do limiar" (veja [Alertas e e-mails](alertas.md)). Quem não é Financeiro vê só a frase "Alerta de caixa: limiar atual …" ou "sem limiar definido". O limiar compara com o **Disponível**, que inclui o valor reservado. Se quiser um alerta sobre o caixa realmente livre, defina o limiar já somando o que costuma ficar reservado. #### Saques retidos aguardando despacho Esta tabela lista até 50 saques que foram **aprovados** mas cujo envio ao PSP foi **adiado**. Colunas: o saque (`external_id` e identificador), o valor, as tentativas, a **próxima tentativa** e o **motivo provável**: - `Falta de caixa`, quando o valor do saque passa do **Disponível** da moeda; - `Janela ou motor de regras`, nos demais casos (por exemplo, fora do horário do automático). O Dracma não grava o motivo do adiamento: "falta de caixa" é uma **inferência** da tela. Tente de novo mais tarde, ou aumente o caixa recebendo depósitos. Veja [Ciclo do saque](../dinheiro/ciclo-do-saque.md). #### Faixa "Saques em verificação" Se o Dracma colocar os saques novos da operação em verificação, uma faixa aparece no alto de **todas** as telas da operação: "Os novos saques desta operação estão em verificação desde … e podem demorar mais que o normal para sair. Os depósitos seguem normalmente." A faixa não informa o motivo. Os depósitos e os saques já em andamento não mudam. Se a faixa durar mais do que você esperava, acione o suporte com a hora que ela mostra. #### Saldo no PSP e aporte de caixa !!! warning "Não disponível" A tela **não mostra o saldo das contas dos PSPs**: o Dracma não consulta o saldo da conexão. A seção "Saldo por conexão de PSP" avisa "Indisponível". O caixa da operação é o do livro do Dracma. !!! warning "Não disponível" O painel não permite **aportar caixa**. O caixa da operação cresce com os depósitos pagos e com as devoluções. Veja [Saldos e caixa livre](../dinheiro/saldos.md#caixa-inicial-e-aporte). ### Exemplos Cartão de uma moeda, com dados fictícios: | Linha | Valor | |---|---| | Disponível | R$ 50.000,00 | | A receber | R$ 4.000,00 | | A pagar | R$ 12.000,00 | | Saída prevista | R$ 20.000,00 (2 aprovados: R$ 8.000,00; 3 em revisão: R$ 12.000,00) | | Disponível projetado | R$ 30.000,00 | Leitura conservadora do mesmo cartão: `50.000 − 12.000 − 20.000 = R$ 18.000,00` de caixa livre depois de pagar a saída prevista. ### Erros e casos de borda | Mensagem | Causa | O que fazer | |---|---|---| | "Limiar de caixa definido." / "Limiar de caixa removido." | Sucesso. | Nenhuma. | | "Valor inválido. Use o formato 1.234,56." | Valor do limiar fora do formato. | Corrija. | | "Moeda inválida." | Moeda fora do padrão. | Recarregue a tela. | | "Confirme com o fator forte para concluir." | `step_up_required`. | Use **Confirmar identidade agora**. | | "Sem permissão para esta ação." | Você não é Financeiro. | Peça a um Financeiro. | | "Serviço indisponível no momento. Tente de novo em instantes." | O livro do Dracma não respondeu. | Repita em instantes. | Casos de borda: - **Livro fora do ar.** A tela não inventa saldo: ela responde "indisponível" até o livro voltar. Isso não afeta depósitos e saques em andamento. - **Caixa livre negativo.** Estornos, `chargeback` e depósitos `returned` com saques reservados podem tornar o caixa livre negativo; novos saques passam a receber `422` com `insufficient_operation_balance`. Veja [Saldos e caixa livre](../dinheiro/saldos.md#caixa-livre-negativo). - **Valor "projetado" negativo.** A tela só avisa. Nada é bloqueado por causa dele. ### Veja também - [Saldos e caixa livre](../dinheiro/saldos.md) - [Reconciliação no painel](reconciliacao.md) - [Alertas e e-mails](alertas.md) - [Limites do jogador e saldos](../referencia/jogadores-e-saldos.md) --- # Arquivo: docs/painel/reconciliacao.md ## Reconciliação no painel > Para: Fin — Conferido em 2026-10-18. A tela **Reconciliação** mostra o fechamento de cada dia (UTC) e moeda, deixa você baixar o mesmo arquivo da API pública, compara o dia com o **seu** livro e organiza o tratamento das divergências. Ela prova que o livro do Dracma bate com o seu. Ela **não** prova que o dinheiro está na conta do PSP. ### O que é O relatório do dia é o mesmo da API (`GET /v1/reconciliation`): os itens liquidados no dia UTC, os totais, o `digest` e a assinatura. A tela acrescenta a conferência com o seu livro e o tratamento das divergências. As regras do relatório (tipos de item, totais, digest, assinatura) estão em [Reconciliação (API)](../referencia/reconciliacao.md); o roteiro financeiro está em [Reconciliar com o seu livro](../dinheiro/reconciliacao-do-operador.md). Quem vê: **Administrador do tenant**, **Financeiro** e **Auditor**. Quem importa o seu livro e trata divergências: só o **Financeiro**. O Revisor não abre a tela. Os dias são **UTC**, e a tela diz isso. Um dia fecha em D+1 às 02:00 UTC (23:00 de Brasília): antes disso o painel mostra "O dia … ainda está aberto: o relatório sai a partir das 02:00 UTC do dia seguinte." ### Quando usar - Todo dia, para fechar o dia anterior com o seu livro. - Antes do go-live, para provar pelo menos um dia inteiro reconciliado (veja [Checklist de go-live](../go-live/checklist.md#7-recuperacao-e-reconciliacao)). - Quando o financeiro precisa de um arquivo assinado do dia, para auditoria externa. ### Como fazer #### Ver o dia 1. Abra **Reconciliação** no menu **Plataforma**. O painel abre o dia mais recente já fechado. 2. Use o formulário **Dia (UTC)** e **Moeda** para trocar de dia. 3. A seção **Últimos dias** lista os últimos 10 dias com: | Coluna | O que mostra | |---|---| | Dia (UTC) | A data; clique para abrir. | | Estado | `Aberto`, `Fechado` ou `Indisponível`. | | Itens | Quantos itens o dia tem. | | Depósitos pagos | O total dos depósitos pagos no dia. | | Saques pagos | O total dos saques pagos no dia. | | Divergências abertas | Quantas divergências ainda esperam tratamento. | O cartão **Dia … · moeda** traz os totais **Depósitos pagos**, **Saques pagos**, **Estornos**, **Chargebacks** e **Tarifas**, a contagem por tipo de item (`Depósito`, `Saque`, `Estorno`, `Chargeback`, `Tarifa`, `Devolução`, `Recall`, `Recebimento`) e o `digest` (sha256) do dia. Em **Itens do dia** aparecem os 200 primeiros itens (item, tipo, estado, valor e quando foi liquidado); o resto sai pela exportação. O Dracma não desconta tarifa nos valores que credita, por isso a linha **Tarifas** vem zerada. Veja [Tarifas](../dinheiro/tarifas.md#tarifa-do-dracma). #### Exportar Os botões **Exportar CSV** e **Exportar JSON** baixam o arquivo do dia, idêntico ao da API pública. O `digest` e a assinatura do arquivo vão nos cabeçalhos da resposta de download (`Dracma-Digest` e `Dracma-Signature`), para você provar a origem. Para exportar, o dia precisa estar fechado; dia aberto responde "Dia ainda aberto." #### Conferir com o seu livro O Financeiro envia o livro do operador do mesmo dia e moeda e o painel compara. 1. Monte um CSV com as colunas: - `kind`: `deposit`, `withdrawal`, `refund` ou `chargeback`; - `external_id`: o identificador do seu sistema; - `amount`: **inteiro na menor unidade**, como na API (`1000` = R$ 10,00). O separador pode ser vírgula ou ponto e vírgula. Linhas repetidas do mesmo item são somadas. O limite da tela é de **900 KB** e **20 mil linhas**. 2. Em **Conferir com o livro do operador**, escolha o arquivo e clique em **Pré-visualizar**. O painel compara **sem gravar nada** e mostra "N linha(s) no livro × M item(ns) no Dracma: tudo bate" ou o número de divergências, com as primeiras. 3. Se o arquivo tiver linhas inválidas, a prévia lista linha, campo e motivo (até 20) e **não** deixa registrar. Corrija e envie de novo. 4. Quando a prévia estiver correta, clique em **Registrar conferência**. As divergências passam a existir na lista do dia. O arquivo **não é guardado**: o painel registra só o resultado da conferência, com o nome do arquivo, o número de linhas e a soma de verificação, e a ação fica na [Auditoria](auditoria.md). Tipos de divergência: | Situação | Significa | |---|---| | No Dracma e não no livro do operador | O Dracma tem o item e o seu livro não. | | No livro do operador e não no Dracma | O seu livro tem o item e o Dracma não. | | Valor diferente | Os dois têm o item, com valores diferentes. | Só os tipos que você consegue conferir pelo `external_id` entram (depósito, saque, estorno e chargeback). Tarifas, devoluções e recebimentos não entram na comparação. #### Tratar as divergências A seção **Divergências do dia** mostra cada uma com a situação, o item, o valor no Dracma, o valor no livro, o estado e o dono. Estados: `Aberta`, `Com dono`, `Resolvida` e `Sumiu na reconferência` (a divergência deixou de existir quando você enviou o livro de novo). 1. **Definir dono:** informe o identificador (UUID) de uma pessoa com permissão para reconciliar; o campo já vem com o seu. Não pede confirmação recente. 2. **Resolver:** escolha uma resolução, escreva o motivo (de 10 a 1000 caracteres) e clique em **Resolver**. É preciso confirmação recente. Resoluções: `Diferença aceita`, `Corrigido no Dracma`, `Corrigido no livro do operador` e `Incidente aberto`. Enviar o livro do mesmo dia outra vez **reconfere**: as divergências que continuam mantêm dono e tratamento; as que sumiram viram `Sumiu na reconferência`. Quando a conferência registra divergências, o alerta "Divergência de reconciliação" avisa a equipe (veja [Alertas e e-mails](alertas.md)). #### O que o painel não prova !!! warning "Não disponível" A conferência automática com o **extrato de cada PSP** não está disponível: o Dracma não importa nem guarda esse extrato. A tela diz: "Enquanto isso, confira pelo livro do operador abaixo; o tratamento de divergências é o mesmo." O relatório prova o **livro do Dracma**, não o saldo nem o extrato no banco ou no PSP. Para conferir o banco, compare o extrato do PSP com o seu livro por fora do painel. O limiar de alerta de caixa baixo fica em [Saldos e caixa](saldos-e-caixa.md). ### Exemplos Livro do operador de um dia, com dados fictícios: ```text kind,external_id,amount deposit,dep-ext-0001,20000 deposit,dep-ext-0002,5000 withdrawal,wdr-ext-0001,150000 refund,dep-ext-0001,2000 ``` Se os demais itens batem e o Dracma tiver, no mesmo dia, o depósito `dep-ext-0003` de `3000`, o saque `wdr-ext-0001` de `150500` e nenhum estorno, a prévia mostra três divergências: | Situação | Item | No Dracma | No livro | |---|---|---|---| | No Dracma e não no livro do operador | Depósito `dep-ext-0003` | R$ 30,00 | — | | Valor diferente | Saque `wdr-ext-0001` | R$ 1.505,00 | R$ 1.500,00 | | No livro do operador e não no Dracma | Estorno `dep-ext-0001` | — | R$ 20,00 | ### Erros e casos de borda | Mensagem | Causa | O que fazer | |---|---|---| | "Este dia ainda está aberto: a reconciliação sai a partir das 02:00 UTC do dia seguinte." | `day_open`. | Volte depois das 02:00 UTC. | | "Arquivo grande demais (até 900 KB e 20 mil linhas)." | `payload_too_large`. | Divida o arquivo por tipo de item. | | "O arquivo tem linhas inválidas. Corrija e envie de novo." | `import_invalid`: coluna ausente, tipo desconhecido, valor que não é inteiro. | Veja a lista de linhas da prévia. | | "Escolha um arquivo CSV válido." / "O arquivo está vazio." | Arquivo ausente ou vazio. | Escolha o arquivo certo. | | "Data inválida." / "Moeda inválida." | Filtro fora do padrão. | Use `AAAA-MM-DD` e a moeda da operação. | | "O dono deve ser o identificador (UUID) de um usuário com permissão de conciliar." | Dono inválido. | Use o UUID de um Financeiro. | | "Escolha o tipo de resolução." / "Escreva um motivo de 10 a 1000 caracteres." | Resolução incompleta. | Complete. | | "Esta divergência já foi encerrada." | `invalid_state_transition`. | Recarregue. | | "Divergência não encontrada nesta operação." | Identificador de outra operação. | Recarregue. | | "Confirme com o fator forte para concluir." | `step_up_required` ao resolver. | Use **Confirmar identidade agora**. | | "Serviço indisponível no momento. Tente de novo em instantes." | O livro do Dracma não respondeu. | Repita em instantes. | Casos de borda: - **Dia com mais de 20 mil itens.** O painel não monta o dia: use a API paginada. - **Fuso.** Um depósito pago às 22:00 de Brasília cai no dia UTC seguinte. Reconcilie sempre por dia UTC. - **Livro com outros tipos de linha.** Remova tarifas, devoluções e recebimentos do CSV: só os quatro tipos conferíveis são aceitos. ### Veja também - [Reconciliação (API)](../referencia/reconciliacao.md) - [Reconciliar com o seu livro](../dinheiro/reconciliacao-do-operador.md) - [Saldos e caixa](saldos-e-caixa.md) - [Relatórios](relatorios.md) --- # Arquivo: docs/painel/auditoria.md ## Auditoria > Para: Adm — Conferido em 2026-10-18. A tela **Auditoria** mostra a trilha de tudo o que as pessoas fizeram na operação: quem, quando, qual ação, em qual objeto e com que resultado. A trilha só recebe registros novos, nunca é editada, e cada registro leva o hash do anterior. Você pode verificar a integridade da cadeia e baixar um arquivo assinado para auditoria externa. ### O que é A trilha é **por operação** e **só de acréscimo** (append-only): o banco não aceita alterar nem apagar registros. Cada registro guarda o hash SHA-256 do registro anterior, formando uma cadeia; qualquer alteração, remoção ou inserção no meio quebra a cadeia e é detectada pela verificação. O que entra: ações das pessoas no painel (decisões de revisão, revelação de dados pessoais, criação e revogação de chaves, pedidos e votos, mudanças de regra, importações de reconciliação, exportações) e as recusas (`refused:step_up_required`, por exemplo). **Nunca** entram dados pessoais, segredos nem o texto livre dos motivos. Quem vê o quê: | Papel | O que a tela mostra | |---|---| | Administrador do tenant, Financeiro, Auditor | A trilha **inteira** da operação, a verificação de integridade e a exportação assinada. | | Revisor | Só as **suas próprias ações**, com o aviso "Você vê apenas as suas próprias ações." | O recorte é feito no servidor, não na tela. ### Quando usar - Para saber quem decidiu um saque, revelou um dado ou revogou uma chave. - Para atender a uma auditoria interna ou externa com um arquivo assinado. - Para provar que a trilha não foi alterada. - Para achar o `trace_id` de uma operação e informar ao suporte. ### Como fazer #### Buscar registros 1. Abra **Auditoria** no menu **Administração**. 2. Use os filtros, todos opcionais e de até 128 caracteres: - **Quem:** o identificador (UUID) do usuário. Não aparece para o Revisor. - **Ação:** começa com (por exemplo `key.`). - **Objeto:** contém. - **Resultado:** começa com (por exemplo `refused`). - **De** e **Até:** data e hora **no horário de Brasília**. 3. Clique em **Filtrar**. A tabela lista, da mais nova para a mais antiga, 100 registros por página (use **Mais antigos** para continuar): `#` (posição na cadeia), quando, quem, ação, objeto, resultado e `trace_id`. O resultado vem com selo verde para `ok` e `allow` e vermelho para os demais, sempre acompanhado do texto. Ações que você vai encontrar com frequência: | Ação | O que registra | |---|---| | `review.vote` | Voto em saque ou depósito em revisão. | | `pii.reveal` | Revelação de dados pessoais. | | `key.reveal`, `key.revoke` | Revelação do segredo e revogação de chave. | | `ip.remove` | Remoção de IP da lista. | | `request.propose`, `request.vote`, `request.cancel`, `request.execute` | Pedidos com aprovação: proposta, voto, cancelamento e vigência. | | `webhooks.secret.rotate`, `webhooks.dlq.redeliver`, `webhooks.dlq.resolve` | Rotação de segredo e tratamento da DLQ. | | `test_charge.create`, `test_charge.simulate_pay` | Cobrança de teste. | | `rules.emergency`, `rules.list.add`, `rules.vip.revoke` | Emergência, lista e VIP. | | `recon.import`, `recon.export`, `recon.threshold` | Reconciliação e limiar de caixa. | | `report.export`, `report.export_pii.download` | Exportação de relatórios. | | `audit.search`, `audit.verify`, `audit.export` | Uso da própria auditoria. | A lista não é exaustiva. O resultado traz o desfecho (`ok`, `ok:approve`, `failed`, `refused:`). #### Verificar a integridade Em **Integridade e exportação** (só para quem vê a trilha inteira), clique em **Verificar integridade da cadeia**. O Dracma recalcula a cadeia inteira da operação, do primeiro registro à ponta, e responde: - **"Cadeia íntegra: N registros conferidos, ponta #P."** Está tudo certo. - **"Cadeia QUEBRADA no registro #n (motivo); N registros conferidos antes da quebra. Acione a segurança."** Algo foi alterado. Motivos da quebra: | Motivo na tela | Significa | |---|---| | registro faltando na sequência | Há um buraco na numeração. | | o elo com o registro anterior não confere | Um registro aponta para um anterior diferente. | | o conteúdo do registro foi alterado | O hash do registro não bate com o conteúdo. | | a ponta da cadeia não confere com o registrado | O último registro difere do que o Dracma guardou. | | registros finais foram removidos | A cadeia está mais curta que o registrado. | Uma cadeia quebrada é um incidente de segurança: guarde a tela e o `trace_id` e acione o suporte imediatamente. #### Exportar com assinatura 1. Em **Exportar auditoria assinada**, preencha **Exportar de** e **Exportar até** (horário de Brasília; ambos opcionais). 2. Clique em **Exportar auditoria assinada**. É preciso confirmação recente. Quem exporta: Administrador, Financeiro e Auditor. 3. O navegador baixa um arquivo `.json` e a tela confirma "Arquivo gerado: …". O arquivo é um envelope com o conteúdo (`payload`) e a **assinatura Ed25519** (`signature`). O conteúdo traz o trecho contínuo da cadeia no período, com os hashes de cada registro, e a ponta da cadeia no momento da exportação. Isso permite que uma auditoria externa **verifique o arquivo sem acesso ao seu sistema**: confira a assinatura com a chave pública e recalcule os hashes encadeados. A **chave pública de verificação** aparece abaixo, com o identificador. Use **Baixar chave pública (PEM)** ou **Copiar chave pública**. Para conferir um arquivo, verifique a assinatura Ed25519 do campo `signature` sobre o `payload` com essa chave. Limites: - até **10 000 registros** por exportação; para períodos maiores, estreite o intervalo; - a exportação também é registrada na trilha (`audit.export`): sem registro, não há arquivo. !!! warning "Não disponível" Em ambientes sem chave de assinatura configurada, a tela mostra "Exportação assinada indisponível neste ambiente: a chave de assinatura não está configurada." e "Chave pública indisponível neste ambiente." A busca e a verificação continuam funcionando. ### Exemplos Quem revogou a chave `key_live_…` ontem à tarde? Filtre por **Ação** `key.revoke`, com **De** `17/10/2026 12:00` e **Até** `17/10/2026 18:00` (horário de Brasília). A linha mostra o `#`, o horário, o identificador de quem revogou, o objeto (o `key_id`), o resultado `ok` e o `trace_id`. Linha de exemplo, com dados fictícios: | # | Quando | Quem | Ação | Objeto | Resultado | trace_id | |---|---|---|---|---|---|---| | 1842 | 17/10/2026 15:07 | `3f9a…c21` | `key.revoke` | `key_live_9d1c…` | `ok` | `0a1b2c3d4e5f60718293a4b5c6d7e8f9` | Para provar a uma auditoria que ninguém mexeu nos registros do mês: clique em **Verificar integridade da cadeia**, exporte o mês com **Exportar auditoria assinada** e entregue o arquivo e a chave pública. ### Erros e casos de borda | Mensagem | Causa | O que fazer | |---|---|---| | "O filtro Quem deve ser o identificador (UUID) do usuário." | `Quem` não é um UUID. | Cole o identificador. | | "Data inválida: use dia e hora existentes, no horário de Brasília." | Data inexistente. | Corrija. | | "Cada filtro aceita até 128 caracteres." | Filtro longo. | Encurte. | | "Página inválida: recomece a busca." | Cursor de paginação inválido. | Filtre de novo. | | "O período está invertido: "De" deve ser anterior a "Até"." | Datas trocadas. | Corrija. | | "Período grande demais: a exportação aceita até 10 000 registros. Estreite o período (De/Até)." | Exportação acima do limite. | Estreite o período. | | "Sem permissão para exportar a auditoria." | Seu papel não exporta. | Peça a um Administrador, Financeiro ou Auditor. | | "Sem permissão para verificar a integridade da trilha." | Seu papel vê só as próprias ações. | Peça a quem vê a trilha inteira. | | "Não foi possível verificar a cadeia agora. Tente de novo; se persistir, acione o suporte com o trace_id." | Falha ao verificar. | Repita; se persistir, fale com o suporte. | | "Confirme com o fator forte para concluir." | `step_up_required` ao exportar. | Use **Confirmar identidade agora**. | Casos de borda: - **Revisor não vê as ações dos outros.** É o comportamento esperado; peça a um Administrador, Financeiro ou Auditor. - **Cadeia por operação.** Cada operação tem a própria cadeia; a verificação e a exportação valem para a operação escolhida. - **Ações da equipe do Dracma.** O uso de acesso de emergência por essa equipe gera o alerta "Break-glass do staff", configurável em [Alertas e e-mails](alertas.md); o alerta não tem fonte de dados ativa hoje (veja a página). ### Veja também - [Alertas e e-mails](alertas.md) - [Relatórios](relatorios.md) - [Revisão](revisao-de-depositos-e-saques.md) - [Suporte](../ajuda/suporte.md) --- # Arquivo: docs/painel/relatorios.md ## Relatórios > Para: Fin — Conferido em 2026-10-18. A tela **Relatórios** reúne quatro relatórios da operação (transações, decisões, sinais de PLD e VIP e listas), com filtros, pré-visualização e exportação em CSV ou JSON. Por padrão os dados pessoais saem **mascarados**. Exportar com dados pessoais em claro exige o pedido, a aprovação de outra pessoa e confirmação de identidade, e o arquivo só pode ser baixado uma vez. ### O que é Quem usa: **Administrador do tenant**, **Financeiro** e **Auditor** exportam relatórios mascarados. **Financeiro** e **Auditor** também pedem e baixam a versão com dados pessoais. O Revisor não abre a tela. | Relatório | O que traz | |---|---| | **Transações** | Uma linha por depósito ou saque: ID, tipo, estado, método, moeda, valor (menor unidade), referência do jogador e data, mais os dados do documento e do destino (mascarados). | | **Decisões** | Um resumo das decisões das regras: contagem por motivo, tipo, modo e estado, a taxa de decisões automáticas e o tempo médio de revisão, e a contagem de decisões por pessoa. | | **Sinais AML** | As transações com o sinal de PLD associado, na frente de cada linha. Os parâmetros dos sinais não são expostos. | | **VIP e listas** | As listas de bloqueio, de liberação e os VIPs ativos: tipo, identificador, estado, quem propôs, criação e validade. O valor das entradas nunca aparece. | Limites: | Item | Valor | |---|---| | Período padrão | Últimos 30 dias | | Período máximo de um relatório | 366 dias | | Linhas por exportação | 200 mil | | Pré-visualização | 50 linhas (até 200) | | Validade do download mascarado | 24 horas | | Validade do download com dados pessoais | 1 hora | As datas do filtro são dias **UTC**. A validade do download conta a partir do momento em que o arquivo fica pronto. ### Quando usar - Para enviar ao financeiro ou ao contador o movimento de um período. - Para auditar quem decidiu o quê (relatório de **Decisões**). - Para conferir listas e VIPs ativos. - Para atender a um pedido regulatório que exige dados pessoais, com a trilha de aprovação. Para um fechamento diário que bata com o seu livro, use a [Reconciliação no painel](reconciliacao.md). ### Como fazer #### Pré-visualizar 1. Escolha a aba do relatório. 2. Preencha os filtros que quiser: **De** e **Até** (datas), **Estado**, **Método**, **Moeda** (3 letras, como `BRL`) e, só em Transações, **Tipo** (`deposit` ou `withdrawal`). 3. A tela mostra as primeiras linhas, sempre mascaradas, e avisa se o resultado foi truncado. Se um relatório não puder ser montado (por exemplo, o serviço de regras fora do ar no relatório de listas), a tela mostra uma nota e **não finge** uma lista vazia. #### Exportar mascarado 1. No bloco **Exportar**, escolha o **Formato** (CSV ou JSON) e clique em **Pedir exportação**. Não pede confirmação recente. 2. Um período de até **7 dias** é gerado na hora. Períodos maiores entram em fila e ficam prontos em segundo plano: recarregue a página de status. 3. A exportação aparece em **Minhas exportações** (criação, relatório, formato, situação, expiração, linhas e tamanho). Clique na data para abrir a página de status. 4. Com a situação **Pronto**, clique em **Baixar**. O arquivo mascarado pode ser baixado mais de uma vez, por quem tem acesso, **até expirar (24 horas)**. Depois disso a situação vira **Expirado** e a tela mostra "Este arquivo já foi baixado ou expirou." Peça outra exportação. Situações: `Na fila`, `Gerando`, `Pronto`, `Falhou`, `Expirado` e `Baixado`. #### Exportar com dados pessoais Só **Financeiro** e **Auditor** veem a opção. ```mermaid sequenceDiagram participant A as Quem pede participant P as Painel participant B as Outra pessoa (Financeiro ou Auditor) A->>P: Marca a opção de dados pessoais em claro, motivo e confirmação P-->>B: Pedido em "Pedidos com dados pessoais" B->>P: Aprova com motivo (confirmação recente) P->>P: Gera o arquivo, cifrado em repouso A->>P: Baixa (confirmação recente, uma vez, em até 1 h) ``` 1. No bloco **Exportar**, marque **Incluir dados pessoais em claro**, escreva o **motivo** (de 10 a 1000 caracteres) e clique em **Pedir exportação**. É preciso confirmação recente. 2. O pedido aparece em **Pedidos com dados pessoais**. **Outra pessoa** (Financeiro ou Auditor) escreve o motivo da decisão e clica em **Aprovar** ou **Recusar**, também com confirmação recente. Quem pediu não aprova o próprio pedido. Não há espera de 24 horas. 3. Aprovado, o Dracma gera o arquivo, cifrado enquanto está guardado. 4. Em **Pedidos com dados pessoais**, o link **Ver a exportação gerada** leva à página de status. **Só quem pediu** baixa, com confirmação recente, **uma única vez**, em até **1 hora**. Depois o arquivo é apagado. Os campos em claro do relatório de Transações: documento, nome, chave PIX, nome do pagador e documento do pagador. Cada pedido, voto e download fica na [Auditoria](auditoria.md). !!! warning "Não disponível" Não há como baixar uma segunda vez um arquivo com dados pessoais, nem outra pessoa baixar o arquivo de quem pediu. Se o arquivo expirou ou foi baixado, abra um novo pedido. ### Exemplos Relatório de transações de setembro para o contador, mascarado: 1. Aba **Transações**, **De** `2026-09-01`, **Até** `2026-09-30`, **Moeda** `BRL`. 2. Formato **CSV**, **Pedir exportação**. Como o período passa de 7 dias, o arquivo vai para a fila. 3. Recarregue a página de status até **Pronto** e clique em **Baixar** em até 24 horas. Colunas do CSV de Transações, mascarado: `id`, `kind`, `status`, `method`, `currency`, `amount`, `player_ref`, `created_at`, `document_masked`, `destination_masked`, `payer_document_masked`. O `amount` é inteiro na menor unidade. ### Erros e casos de borda | Mensagem | Causa | O que fazer | |---|---|---| | "Use datas válidas no período." / "A data inicial não pode ser depois da final." | Datas inexistentes ou invertidas. | Corrija. | | "Filtro inválido." / "A moeda deve ter 3 letras, como BRL." | Filtro fora do padrão. | Corrija. | | "Escolha CSV ou JSON." / "Relatório desconhecido." | Parâmetro fora da lista. | Escolha pela tela. | | "Escreva um motivo de 10 a 1000 caracteres." | Falta o motivo (obrigatório com dados pessoais). | Escreva. | | "Exportação criada. Ela é gerada em segundo plano." | Sucesso (mascarado). | Acompanhe em **Minhas exportações**. | | "Pedido enviado. Precisa da aprovação de outra pessoa antes de gerar o arquivo." | Sucesso (dados pessoais). | Peça a outra pessoa que aprove. | | "O arquivo ainda não está pronto." | `export_not_ready`: o job está na fila ou gerando. | Recarregue em instantes. | | "Este arquivo já foi baixado ou expirou." | `gone`. | Peça outra exportação. | | "A geração falhou. Peça a exportação de novo." | O job falhou. | Repita. | | "Quem pediu não pode aprovar o próprio pedido." | `self_review_forbidden`. | Peça a outra pessoa. | | "Você já votou neste pedido." | `already_voted`. | Nenhuma. | | "Confirme com o fator forte para concluir." | `step_up_required`. | Use **Confirmar identidade agora**. | | "Sem permissão para esta ação." | Seu papel não permite. | Peça a Financeiro ou Auditor. | Casos de borda: - **Tenant sem Financeiro nem Auditor.** Nenhuma exportação com dados pessoais é possível; o Administrador exporta só mascarado. - **Período maior que 366 dias.** É recusado; divida em períodos. - **Mais de 200 mil linhas.** A geração falha ("A geração falhou. Peça a exportação de novo."). Estreite os filtros e peça de novo. - **Relatório de listas "indisponível".** O serviço de regras não respondeu; repita em instantes. ### Veja também - [Transações](transacoes.md) - [Reconciliação no painel](reconciliacao.md) - [Auditoria](auditoria.md) - [Dados pessoais](../seguranca/dados-e-privacidade.md) --- # Arquivo: docs/painel/alertas.md ## Alertas e e-mails > Para: Adm · Fin — Conferido em 2026-10-18. A tela **Alertas** liga e desliga 12 tipos de aviso da operação (caixa baixo, fila de revisão, webhook falhando, divergência de reconciliação e outros), escolhe por onde cada um sai (e-mail, webhook ou os dois) e guarda o histórico dos disparados. Esta página também lista os e-mails que o Dracma envia aos contatos da operação. ### O que é Cada operação tem uma **regra** por tipo de alerta. Uma regra tem: ligada ou desligada, o **limiar** (nos tipos que têm), os **canais** e o **silêncio** entre alertas iguais. **Toda regra começa desligada**, com o canal e-mail e silêncio de 60 minutos. Quem vê as regras e o histórico: todos os papéis com acesso à operação. Quem **configura**: Administrador do tenant e Financeiro; os demais veem a regra em uma linha ("Ligada · limiar 50 itens · e-mail + webhook · silêncio 1 h"). Os 12 tipos: | Tipo | Severidade | Como dispara | O que avisa | |---|---|---|---| | Caixa abaixo do limiar | Crítico | Periódico | O **Disponível** da moeda ficou abaixo do limiar de [Saldos e caixa](saldos-e-caixa.md). Não tem limiar próprio; configura-se a moeda (padrão `BRL`). | | SLA de revisão estourado | Atenção | Periódico | Há N itens em revisão há mais de 24 horas. O limiar é N. | | Fila de revisão acima de N itens | Atenção | Periódico | Saques e depósitos esperando revisão acima do limiar. | | Taxa de automático variou | Atenção | Periódico | A taxa de saques decididos automaticamente na última hora variou mais que o limiar (em %) em relação à hora anterior. Exige uma amostra mínima de saques por hora (de 1 a 1000). | | Conexão de PSP com circuit breaker aberto | Crítico | Periódico | Uma conexão de PSP parou de receber tentativas. | | Entrega de webhook falhando | Atenção | Periódico | A entrega ao webhook está sem sucesso há mais que o limiar (em minutos), ou algum evento foi para a DLQ nas últimas 24 horas. | | Certificado a vencer | Atenção | Periódico | Um certificado de conexão está perto do vencimento. | | Rajada de 401/403 na API | Atenção | Periódico | Muitas recusas de autenticação em pouco tempo (chave vazada?). | | Mudança de regra ativada | Informativo | Por evento | Uma versão de regras entrou em vigor. | | Uso do interruptor de emergência | Crítico | Por evento | **Suspender automático** foi acionado. | | Break-glass do staff | Crítico | Por evento | A equipe do Dracma usou acesso de emergência na operação. | | Divergência de reconciliação | Crítico | Por evento | A conferência com o seu livro encontrou divergência. | !!! warning "Não disponível" Quatro tipos **não têm fonte de dados** hoje: conexão de PSP com circuit breaker aberto, certificado a vencer, rajada de 401/403 e break-glass do staff. A tela mostra o selo **Sem fonte de dados ainda** e a nota "A regra pode ser configurada agora, mas só dispara quando o dado existir." Você pode salvar a regra, mas nenhum alerta sai por ela hoje. Os demais oito mostram **Fonte de dados ativa**. ### Quando usar Ligue pelo menos, antes do go-live: - **Caixa abaixo do limiar**, depois de definir o limiar de cada moeda; - **Entrega de webhook falhando**; - **Fila de revisão acima de N itens** e **SLA de revisão estourado**; - **Divergência de reconciliação**. ### Como fazer #### Configurar uma regra 1. Abra **Alertas** no menu **Administração**. Cada tipo é um cartão. 2. Marque **Regra ligada**. 3. Se o tipo tem limiar, preencha **Limiar** (número decimal; a unidade aparece no campo). 4. Em **Canais**, marque **E-mail aos contatos de alerta da operação**, **Webhook cadastrado da operação** ou os dois. Uma regra ligada precisa de pelo menos um canal. 5. Em **Silêncio entre alertas iguais**, informe os minutos (de 1 a 10080, isto é, até 7 dias). Enquanto a janela não termina, o mesmo alerta (do mesmo tipo e para o mesmo item, como a mesma moeda) não se repete. 6. Clique em **Salvar regra**. Não pede confirmação recente; a mudança fica na [Auditoria](auditoria.md). No cartão aparecem também a data e o autor da última atualização. #### Canais de saída - **E-mail:** vai aos contatos de alerta da operação, em texto puro, com o assunto `Alerta: ` e os detalhes. - **Webhook:** o Dracma envia ao endereço de webhook da operação um evento `alert.fired`, assinado como qualquer entrega. Ele **não faz parte do catálogo** de depósito e saque, e a escolha de eventos da operação não o filtra. O corpo traz, em `data`: `alert_id`, `kind`, `severity`, `title`, `detail`, `value`, `threshold` e `fired_at`. O seu receptor deve **aceitar e responder `2xx`** a tipos de evento que não conhece (veja [Receptor passo a passo](../webhooks/receptor.md)). Na aba **Entregas** de [Webhooks no painel](webhooks.md), uma entrega `alert.fired` pode aparecer como **Ignorado**: é um alerta cuja regra não escolheu o canal webhook. #### Contatos de alerta Os e-mails vão para os **contatos de alerta** da operação: **até 5 endereços**, cadastrados pelo Dracma no onboarding. !!! warning "Não disponível" O painel **não edita** os contatos de alerta. Uma operação criada pelo painel nasce **sem contatos** até o Dracma cadastrá-los, e sem contatos nenhum e-mail de alerta nem o aviso de "informação solicitada" sai (o pedido mostra "Operação sem contatos de e-mail cadastrados"). Informe os endereços no onboarding e, para trocar ou remover, peça ao suporte do Dracma. Veja [O que preparar](../onboarding/pre-requisitos.md). #### E-mails que o Dracma envia à operação Todos os e-mails são em **texto puro**, nunca trazem dados pessoais do jogador e têm um rodapé que explica por que o endereço os recebe. Na operação de Teste o assunto começa com `[Dracma sandbox]`. | E-mail | Quando sai | O que traz | |---|---|---| | **Webhook em DLQ** | Uma entrega esgotou as tentativas e foi para a DLQ. | Operação, ambiente, evento, entrega, número de tentativas e o último HTTP. Avisa que o evento **não foi perdido** e que a reentrega é pela DLQ. Assunto: `Webhook em DLQ: `. | | **Saque aguardando revisão manual** | Um saque foi para revisão. | Operação, ambiente, saque, valor (na menor unidade), horário do evento. Pede para decidir no painel. | | **Resumo diário** | Todo dia, a partir das **06:00 (America/Sao_Paulo)**, referente ao **dia anterior** (dia de Brasília). | Por tipo de evento: quantidade e soma por moeda. O assunto avisa que **não é a reconciliação oficial**. | | **Alerta** | Um alerta com canal e-mail disparou. | O tipo e a severidade, o horário, o identificador do alerta, o valor e o limiar (quando houver) e o detalhe. | | **Informação solicitada** | Um revisor pediu informação sobre um saque ([Revisão](revisao-de-depositos-e-saques.md)). | O texto do revisor e o identificador do saque no seu sistema. | Um mesmo fato gera **um** e-mail (o resumo, um por dia; a DLQ, um por entrega). O resumo diário **não** substitui a [Reconciliação no painel](reconciliacao.md): use-o só como aviso. !!! note "Fora da produção" Fora da produção esses e-mails não chegam a destinatários reais: ficam retidos num destino de teste. Não use o e-mail como único meio de saber de um problema na homologação; consulte as telas. #### Histórico Abaixo das regras, o **Histórico** lista os alertas disparados (20 por vez), com a data, o título, a severidade, o detalhe, o valor e o limiar, e os canais por onde saíram. ### Exemplos Aviso de fila grande, enviado por e-mail e webhook: | Campo | Valor | |---|---| | Regra | Fila de revisão acima de N itens | | Regra ligada | sim | | Limiar (itens) | `20` | | Canais | E-mail e Webhook | | Silêncio | `120` minutos | Com essa regra, se a fila passar de 20 itens, os contatos recebem um e-mail "Alerta: Fila de revisão acima de N itens" e o receptor recebe um `alert.fired`. Enquanto o problema continuar, o aviso se repete no máximo uma vez a cada 2 horas. ### Erros e casos de borda | Mensagem | Causa | O que fazer | |---|---|---| | "Regra salva." | Sucesso. | Nenhuma. | | "O período de silêncio deve ser um número de minutos entre 1 e 10080." | Silêncio fora da faixa. | Corrija. | | "Escolha ao menos um canal (e-mail ou webhook) para uma regra ligada." | Regra ligada sem canal. | Marque um canal. | | "Informe um limiar numérico válido para este tipo." | Limiar ausente ou inválido; ou limiar num tipo que não tem. | Corrija. | | "Parâmetro extra inválido (moeda ou amostra mínima)." | Moeda ou amostra fora do formato. | Corrija. | | "A api recusou algum campo da regra. Revise os valores." | `validation_failed`. | Revise. | | "Sem permissão para configurar alertas nesta operação." | Seu papel só vê. | Peça a um Administrador ou Financeiro. | | "Serviço de autorização indisponível. Tente de novo em instantes." | Dependência fora do ar. | Repita. | Casos de borda: - **Alerta de caixa não dispara.** Confira se a moeda tem limiar definido em [Saldos e caixa](saldos-e-caixa.md). - **Alerta ligado, nada chega.** Veja o histórico; se o alerta aparece lá com canal e-mail e o e-mail não chegou, a operação provavelmente está sem contatos. - **Muitos alertas iguais.** Aumente o silêncio. - **Para parar de receber e-mails** num endereço, peça a remoção ao suporte. ### Veja também - [Saldos e caixa](saldos-e-caixa.md) - [Webhooks no painel](webhooks.md) - [Reconciliação no painel](reconciliacao.md) - [Suporte](../ajuda/suporte.md) --- # Arquivo: docs/painel/erros-do-painel.md ## Mensagens do painel > Para: Adm — Conferido em 2026-10-18. Quando uma ação do painel falha, a tela mostra uma mensagem curta em português. Esta página reúne as mensagens mais comuns, a causa e o que fazer. As mensagens próprias de cada tela também estão na seção "Erros e casos de borda" da página dela. ### O que é O painel traduz a resposta do Dracma em um **texto fixo**: ele nunca mostra o texto cru de um erro, nem repete o que você digitou. Por trás de cada mensagem há um **código**, que é o mesmo que o suporte vai pedir. Os códigos seguem a família de status: | Status | Quando | |---|---| | `400` | Dados fora do formato (`validation_failed`). A tela diz qual campo. | | `401` | Falta confirmação recente de identidade (`step_up_required`) ou a sessão expirou. | | `403` | Seu papel não permite (`forbidden`). | | `404` | O item não existe nesta operação **ou** você não pode vê-lo (`not_found`). | | `409` | O estado mudou ou a regra de aprovação barrou (`self_review_forbidden`, `already_voted`, `invalid_state_transition` e outros). | | `410` | O prazo passou (`reveal_expired`, `already_revealed`, `gone`). | | `503` | Um serviço de que a tela depende não respondeu (`service_unavailable`). | ### Quando usar Use esta página quando uma ação falhar, para achar a causa e o próximo passo. Se a sua mensagem não estiver aqui, procure na página da tela ou acione o suporte. ### Como funciona #### Confirmação, permissão e sessão | Mensagem | Código | Causa | O que fazer | |---|---|---|---| | "Confirme com o fator forte para concluir." | `step_up_required` | A ação exige passkey ou aplicativo autenticador confirmados nos últimos 5 minutos. | Use **Confirmar identidade agora** e repita. | | "Sem permissão para esta ação." | `forbidden` | Seu papel não permite a ação. | Peça a quem tem o papel (veja [Equipe e papéis](equipe-e-papeis.md)). | | "Não encontrado. O item não existe ou você não tem acesso a ele." | `not_found` | Operação ou item de outro tenant, sem permissão, ou inexistente. | Confira a operação no seletor e o seu papel. | | "Você ainda não tem acesso a nenhuma operação. Peça ao administrador do seu tenant." | — | Sua conta existe, mas sem papel em nenhuma operação. | Peça a um Administrador que conceda um pacote. | | "O login foi cancelado ou recusado." / "O pedido de login expirou. Tente de novo." | — | O login foi interrompido. | Clique em **Entrar** e refaça. | | "Sua sessão expirou. Entre de novo." | — | Sessão encerrada. | Entre de novo. | #### Aprovação de outra pessoa | Mensagem | Código | Causa | O que fazer | |---|---|---|---| | "Quem pediu não pode aprovar o próprio pedido." | `self_review_forbidden` | Quem propôs tentou votar. | Peça a outra pessoa. | | "Você já votou neste pedido." | `already_voted` | Voto repetido. | Nenhuma. | | "Este item já foi decidido." / "Este pedido já foi encerrado ou não aceita essa ação agora." | `invalid_state_transition` | Outra pessoa decidiu antes, o prazo passou, ou o pedido foi cancelado. | Recarregue a página. | | "Escreva um motivo de 10 a 1000 caracteres." | `motivo_invalido` | Todo voto, proposta e cancelamento exige motivo. | Escreva o motivo. | | "Não é possível remover o último administrador do tenant." | `last_admin` | A remoção deixaria o tenant sem Administrador. | Conceda o pacote a outra pessoa antes. Veja [Equipe e papéis](equipe-e-papeis.md). | #### Chaves, IPs e operações | Mensagem | Código | Causa | O que fazer | |---|---|---|---| | "A operação já tem 2 chaves ativas. Revogue uma antes de criar outra." | `key_limit_reached` | Limite de 2 chaves ativas. | Veja [Chaves de API](chaves-de-api.md). | | "O prazo para revelar a chave venceu." | `reveal_expired` | Passaram 24 horas desde que a chave nasceu. | Peça outra chave. | | "A chave já foi mostrada e não pode ser mostrada de novo." | `already_revealed` | O segredo é exibido uma vez. | Peça outra chave. | | "Não há segredo a revelar: já foi mostrado, a janela venceu ou o pedido não é seu." | `not_found` | Mesma família, na tela de chaves. | Peça outra chave. | | "Este IP já está na lista." | `already_exists` | Entrada repetida. | Nenhuma. Veja [IPs do conector](allowlist-de-ip.md). | | "IP ou CIDR inválido (IPv4 de /24 a /32, IPv6 de /56 a /128)." | `cidr_invalido` | Formato ou tamanho de rede fora do aceito. | Corrija. | | "Já existe um tenant com esse nome." / "Já existe uma operação com esse nome neste tenant." | `tenant_exists` / `operation_exists` | Nome em uso (a unicidade vale para todo o Dracma). | Escolha outro nome. | | "Este tipo de operação não é permitido neste ambiente." | `environment_not_allowed` | Teste em produção ou Real onde não é habilitado. | Veja [Tenants e operações](tenants-e-operacoes.md). | | "Executor indisponível agora" | `service_unavailable` | O serviço de gateways não respondeu. | Repita em instantes. | #### Webhooks e cobrança de teste | Mensagem | Código | Causa | O que fazer | |---|---|---|---| | "O teste de URL só existe em operações sandbox." / "Simular pagamento só existe em operações sandbox." | `sandbox_only` | Ação só da operação de Teste. | Use a operação de Teste. | | "URL inválida: use https, sem usuário e senha na URL." | `url_invalida` | URL fora do formato. | Veja [Webhooks no painel](webhooks.md). | | "A api recusou os dados enviados. Confira os campos." | `validation_failed` | A checagem de URL, motivo ou pagador falhou. | Revise os campos. | | "A operação exige dados adicionais do pagador (por exemplo e-mail). Confira os campos." | `requisitos` | A operação exige um dado que a tela não preencheu. | Preencha ou use a API. Veja [Cobrança de teste](cobranca-de-teste.md). | | "O serviço de webhooks está indisponível agora." | `service_unavailable` | O serviço de entrega não respondeu. | Repita em instantes. | #### Revisão, regras e reconciliação | Mensagem | Código | Causa | O que fazer | |---|---|---|---| | "Não foi possível concluir a ação." ao **Aceitar** um depósito | `accept_not_allowed` | O depósito está em revisão por valor pago diferente (`AMOUNT_MISMATCH`): aceitar é proibido. | Use **Devolver**. Veja [Revisão](revisao-de-depositos-e-saques.md). | | "A regra mudou desde que você abriu a tela. Recarregue a página e refaça a edição." | `etag_mismatch` | Outra mudança entrou em vigor. | Recarregue. Veja [Regras, listas e emergência](regras-e-listas.md). | | "Marque que leu o relatório da simulação para propor um afrouxamento." | `dry_run_ack_obrigatorio` | Falta a caixa na mudança relaxante. | Marque a caixa. | | "Este dia ainda está aberto: a reconciliação sai a partir das 02:00 UTC do dia seguinte." | `day_open` | O dia UTC ainda não fechou. | Volte depois das 02:00 UTC. Veja [Reconciliação no painel](reconciliacao.md). | | "Arquivo grande demais (até 900 KB e 20 mil linhas)." | `payload_too_large` | O CSV passou do limite. | Divida o arquivo. | | "O arquivo tem linhas inválidas. Corrija e envie de novo." | `import_invalid` | Linhas fora do formato. | Veja a lista de linhas da prévia. | | "Este arquivo já foi baixado ou expirou." | `gone` | Download único ou prazo vencido. | Peça outra exportação. Veja [Relatórios](relatorios.md). | | "O arquivo ainda não está pronto." | `export_not_ready` | A exportação está na fila. | Recarregue em instantes. | | "Exportação assinada indisponível neste ambiente: a chave de assinatura não está configurada." | `audit_signing_unavailable` | Sem chave de assinatura no ambiente. | Acione o suporte. Veja [Auditoria](auditoria.md). | | "O estado mudou e a ação não vale mais. Recarregue a página." | `conflict` | Outra pessoa mudou o item. | Recarregue. | #### Falhas gerais | Mensagem | Causa | O que fazer | |---|---|---| | "Serviço indisponível agora. Tente de novo em instantes." | Um serviço de que a tela depende (autorização, livro, entrega) não respondeu (`service_unavailable`). | Repita em instantes. | | "Não foi possível concluir a ação." | Erro sem texto próprio. | Repita; se persistir, acione o suporte. | | "Atualizando a página…" | A página foi aberta antes de uma atualização do painel. | Aguarde: ela recarrega sozinha. | | "Algo deu errado. Não foi possível concluir. Tente de novo." | Falha inesperada. | Use **Tentar de novo**; se persistir, acione o suporte. | ### Exemplos Você pede uma chave e a tela responde "Pedido de chave enviado. Precisa da aprovação de outra pessoa e do time-lock de 24 h." Isso **não é um erro**: é a confirmação de que o pedido foi aberto. Em seguida outro Administrador aprova e, 24 horas depois, a chave nasce. Você tenta aprovar o seu próprio pedido e vê "Quem pediu não pode aprovar o próprio pedido." (`self_review_forbidden`). A correção é pedir a outra pessoa. ### Erros e casos de borda - **Mensagem genérica.** "Não foi possível concluir a ação." aparece quando o código não tem texto próprio. Repita uma vez; se persistir, acione o suporte. - **Erro depois de uma ação que parecia ter funcionado.** Confira na [Auditoria](auditoria.md): se o registro da ação está lá com `ok`, ela foi concluída. Recusas aparecem como `refused:`. - **O que enviar ao suporte.** O nome da operação, a tela, a mensagem exata, o horário em UTC e o `trace_id` da linha da [Auditoria](auditoria.md), quando houver. Nunca envie o segredo de uma chave. ### Veja também - [O painel](README.md) - [Equipe e papéis](equipe-e-papeis.md) - [Suporte](../ajuda/suporte.md) - [Erros](../api/erros.md) --- # Arquivo: docs/api/autenticacao.md ## Autenticação HMAC > Para: Dev — Conferido em 2026-10-18. Toda chamada à API `/v1` leva uma assinatura HMAC-SHA256 calculada sobre o método, o caminho, o corpo, um carimbo de tempo e um nonce. Esta página mostra os cabeçalhos, o pedido canônico, o exemplo trabalhado (com os vetores públicos), a ordem em que o servidor confere e o que cada `401` e `403` significa. ### O que é A chave de API tem duas partes: o **`key_id`** (`key_test_…` ou `key_live_…`, público, vai no cabeçalho) e o **segredo** (`sk_test_…` ou `sk_live_…`, que nunca sai do seu servidor). O segredo assina; o `key_id` diz ao Dracma com qual segredo conferir. De posse de uma assinatura válida, o Dracma sabe **qual operação** está chamando: tenant e operação vêm só da chave, nunca do corpo. A assinatura é a camada de autenticação que vale em toda chamada. Camadas de rede opcionais (mTLS e lista de IPs) estão descritas em [Rede, lista de IPs e mTLS](../seguranca/rede-allowlist-e-mtls.md). !!! warning "Não disponível" O Dracma não aplica a lista de IPs às chamadas hoje. A tela de IPs do painel registra a lista, mas as chamadas não são recusadas por IP. Não construa o seu controle de acesso em cima de `403 ip_not_allowed`: ele existe no catálogo de erros, mas você não deve vê-lo. Veja [IPs do conector](../painel/allowlist-de-ip.md#ips-do-conector). Cada chave também tem **escopos**: uma chave só de leitura não cria depósito (seção Escopos, abaixo). ### Quando usar Em toda chamada a `/v1/*`, inclusive `GET /v1/ping` e as rotas `/v1/sandbox/*`: nenhuma rota de `/v1` dispensa assinatura. O documento OpenAPI não passa pela API; baixe-o em [OpenAPI](../referencia/openapi.md). ### Como funciona #### Os quatro cabeçalhos ```text Dracma-Key-Id: key_test_01J9ABCDEF (key_live_… em operação Real) Dracma-Timestamp: 1790000000 (segundos Unix, UTC) Dracma-Nonce: 0123456789abcdef0123456789abcdef Dracma-Signature: v1=a58fa1956941157aad72536512b2b09f234de3670050c865071ddc56df3b4964 ``` | Cabeçalho | Formato exato aceito | |---|---| | `Dracma-Key-Id` | `key_test_` ou `key_live_` seguido de 1 a 64 caracteres `[A-Za-z0-9]` | | `Dracma-Timestamp` | só dígitos ASCII, sem sinal, sem espaço e sem zero à esquerda; em **segundos** (não milissegundos) | | `Dracma-Nonce` | exatamente 32 caracteres hexadecimais **minúsculos** (`^[0-9a-f]{32}$`), isto é, 16 bytes de gerador criptográfico | | `Dracma-Signature` | `v1=` seguido de 64 hexadecimais minúsculos | Todo `POST` leva também `Idempotency-Key` ([Idempotência e repetição](idempotencia.md)). O prefixo do `key_id` diz em que tipo de operação a chave vale: `key_test_` em operação **Teste** e `key_live_` em operação **Real**. A API que atende o host só aceita o prefixo do seu tipo; o contrário volta `401 unknown_key` ([Ambientes, tipos de operação e hosts](../conceitos/ambientes-e-hosts.md#matriz-instancia-tipo)). #### Pedido canônico e assinatura O pedido canônico tem **seis linhas** separadas por `\n` (LF, byte `0x0A`), **sem `\n` no final**: ```text v1 ``` A assinatura é o HMAC-SHA256 do pedido canônico, com o segredo da chave: ```text assinatura = hex_minúsculo( HMAC-SHA256( segredo, pedido_canonico ) ) Dracma-Signature: v1= ``` O segredo é usado como os **bytes da string** que você recebeu (por exemplo `sk_test_vetor_0123456789abcdef`, com o prefixo `sk_test_`), em UTF-8. Não decodifique de hexadecimal nem de base64. ```mermaid sequenceDiagram participant C as Seu conector participant D as Dracma C->>C: serializa o corpo uma vez, sha256 dos bytes C->>C: timestamp (s), nonce (16 bytes aleatórios), pedido canônico C->>C: HMAC-SHA256 com o segredo da chave C->>D: método, caminho, 4 cabeçalhos, Idempotency-Key (POST), os MESMOS bytes do corpo D->>D: confere formato, janela, chave, assinatura, nonce, escopo D-->>C: resposta (Dracma-Trace-Id em toda resposta) ``` #### As cinco armadilhas que mais quebram integração 1. **O caminho inclui `/v1`.** Se a sua base é `https:///v1` e você chama `/deposits`, o caminho assinado é `/v1/deposits`: o caminho completo que chega ao servidor. 2. **A query vai como enviada.** Não reordene parâmetros, não decodifique `%XX`, não normalize. Monte a URL uma vez e assine a mesma string que o cliente HTTP envia (`/v1/deposits?external_id=dep_x&limit=10`). Desligue qualquer normalização de caminho do seu cliente (`curl --path-as-is`). 3. **Assine os bytes que você envia.** Serialize o JSON **uma vez**, calcule o sha256 sobre esses bytes e mande esses mesmos bytes. Reserializar (outra ordem de chaves, outro espaçamento, outro escape de Unicode) muda o hash. 4. **Nonce hexadecimal minúsculo, de gerador criptográfico.** `crypto/rand`, `crypto.randomBytes`, `random_bytes`, `openssl rand -hex 16`. Nunca `Math.random()` nem `math/rand`. Um nonce com letra maiúscula é recusado. 5. **Timestamp em segundos.** `Date.now()` devolve milissegundos: divida por 1000 e trunque. #### Janela de tempo, nonce e anti-replay - **Janela de ±300 s.** `abs(agora do servidor − Dracma-Timestamp)` precisa ser no máximo 300 s, comparado em segundos inteiros. - **Relógio.** Mantenha NTP no host do conector. `GET /v1/ping` devolve `server_time`; compare com o seu relógio e alerte se o desvio passar de 5 s. Um relógio que deriva vira, um dia, 100% de `timestamp_out_of_window`. - **Nonce.** O Dracma guarda `(key_id, nonce)` por 600 s e recusa o par repetido com `401 replayed_nonce`. Não existe teto de nonces por chave: quem limita o volume é o [limite de taxa](limites.md). - **Quando o nonce é consumido.** Só **depois** de a assinatura conferir e de a chave estar utilizável. Uma requisição recusada antes disso (janela, assinatura, chave revogada) **não** queima o nonce. - **Repetir uma chamada é uma chamada nova.** Mesmo depois de um `5xx` ou de um *timeout*, a nova tentativa leva novo `Dracma-Timestamp`, **novo nonce** e nova assinatura, e a **mesma** `Idempotency-Key`. Reusar o nonce dá `replayed_nonce`. Passada a verificação de nonce, qualquer recusa seguinte (`429`, `403`, `400`) também o consome. #### Exemplo trabalhado (vetor normativo 1) Os valores abaixo vêm de [`vetores-assinatura.json`](../exemplos/vetores-assinatura.json). São valores de teste públicos; toda implementação deve reproduzi-los byte a byte. | Entrada | Valor | |---|---| | segredo | `sk_test_vetor_0123456789abcdef` | | `key_id` | `key_test_vetor` | | timestamp | `1790000000` | | nonce | `0123456789abcdef0123456789abcdef` | | método | `POST` | | caminho | `/v1/deposits` | | corpo | `{"external_id":"dep_0192a3b4-5c6d-7e8f-9a0b-1c2d3e4f5a6b","player":{"external_ref":"u-1","document":{"type":"CPF","number":"12345678909","country":"BR"}},"amount":{"amount":"5000","currency":"BRL"},"method":"pix"}` | Passo 1, sha256 do corpo cru: ```text fed01c42dbe44bee43439f86a259262975544b122ac57e75977fb5520bbb6edb ``` Passo 2, pedido canônico (seis linhas, sem LF final): ```text v1 1790000000 0123456789abcdef0123456789abcdef POST /v1/deposits fed01c42dbe44bee43439f86a259262975544b122ac57e75977fb5520bbb6edb ``` Passo 3, HMAC-SHA256 com o segredo: ```text Dracma-Signature: v1=a58fa1956941157aad72536512b2b09f234de3670050c865071ddc56df3b4964 ``` Conferência em uma linha de shell: ```bash printf 'v1\n1790000000\n0123456789abcdef0123456789abcdef\nPOST\n/v1/deposits\nfed01c42dbe44bee43439f86a259262975544b122ac57e75977fb5520bbb6edb' \ | openssl dgst -sha256 -hmac 'sk_test_vetor_0123456789abcdef' # SHA2-256(stdin)= a58fa1956941157aad72536512b2b09f234de3670050c865071ddc56df3b4964 ``` Os outros dois vetores cobrem os casos de borda: | Vetor | Método e caminho | Corpo | O que prova | Assinatura | |---|---|---|---|---| | 2 | `GET /v1/deposits?external_id=dep_x&limit=10` | vazio (sha256 de `""`, começa com `e3b0c442`) | a query entra como enviada; corpo vazio também é assinado | `v1=83b5e8b97bccfc97db1fdaa7f847b2a229a0835b88ce95032ab7c8f951487316` | | 3 | `POST /v1/withdrawals/wdr_01J9ZQ4/cancel` | vazio | `POST` sem corpo também assina o sha256 de `""` | `v1=64df5c1a75e4cfc367e165f55be0d6b50678848069f96abb27a4f2559abdae5d` | O arquivo [`vetores-assinatura-ts-negativos.json`](../exemplos/vetores-assinatura-ts-negativos.json) traz timestamps fora da forma canônica (`+1790000000`, `01790000000`, com espaço, em notação científica) que o Dracma recusa todos com `401 timestamp_out_of_window`. Os clientes de [Exemplos](../exemplos/README.md) (Go, Node/TypeScript, PHP e shell) têm testes que reproduzem os vetores. #### Pseudocódigo ```text função chamar(método, caminho_com_query, corpo_bytes, idempotency_key?): ts = str(floor(agora_unix_segundos())) nonce = hex_minusculo(csprng(16)) canon = join("\n", ["v1", ts, nonce, upper(método), "/v1" + caminho_com_query, sha256_hex(corpo_bytes)]) sig = "v1=" + hex(hmac_sha256(segredo, canon)) headers = {Dracma-Key-Id: key_id, Dracma-Timestamp: ts, Dracma-Nonce: nonce, Dracma-Signature: sig} se método == POST: headers[Idempotency-Key] = idempotency_key envia EXATAMENTE corpo_bytes para base_host + "/v1" + caminho_com_query, com timeout de 10 s ``` #### Ordem de verificação O servidor confere nesta ordem, e o **primeiro** problema encontrado é o que volta. Isso ajuda a diagnosticar: um `403` significa que a autenticação já passou. | # | Verificação | Falha | |---|---|---| | 1 | Corpo de até 64 KiB | `413 validation_failed` (`errors[0].reason = too_large`) | | 2 | Limite de taxa por IP de origem, antes de autenticar | `429 rate_limited` + `Retry-After` | | 3 | `Dracma-Key-Id` bem formado e com o prefixo do tipo de operação deste host. Decidido só pelo cabeçalho, sem consultar a chave | `401 unknown_key` | | 4 | `Dracma-Timestamp` na forma canônica e dentro de ±300 s | `401 timestamp_out_of_window` | | 5 | `Dracma-Nonce` no formato (32 hexadecimais minúsculos) | `401 invalid_signature` | | 6 | A chave existe **e** a assinatura confere. Chave inexistente e assinatura errada dão a **mesma** resposta | `401 invalid_signature`; depois de 20 falhas por minuto do mesmo IP, `429 rate_limited` | | 7 | Só com assinatura válida: a chave está ativa (não revogada) | `401 unknown_key` | | 8 | Só com assinatura válida: IP de origem na lista da operação | `403 ip_not_allowed` (a lista não é aplicada hoje) | | 9 | Só então o nonce é gravado; o par `(key_id, nonce)` já visto nos últimos 600 s | `401 replayed_nonce` | | 10 | Operação ativa | `403 operation_suspended` | | 11 | Limite de taxa por chave e por operação (e o de listagem) | `429 rate_limited` + `Retry-After` | | 12 | O escopo da chave cobre a rota | `403 insufficient_scope` | | 13 | `Idempotency-Key` válida em todo `POST` | `400 validation_failed` (`field: Idempotency-Key`, `reason: required`) | | 14 | Operação Teste com a queda simulada ligada (`/sandbox/outage`), em rota fora de `/sandbox/*` | `503 service_unavailable` + `Retry-After: 5` | Depois disso vêm a validação do corpo (`400`) e as regras da rota. - **Sem oráculo de existência.** Quem não prova a posse do segredo não descobre se uma chave existe nem se foi revogada. `401 unknown_key` por chave revogada só aparece **depois** de a assinatura de uma chave real conferir. - **Cabeçalho ausente.** `Dracma-Key-Id` ausente vira `unknown_key`; timestamp ausente vira `timestamp_out_of_window`; nonce ou assinatura ausentes viram `invalid_signature`. - **O erro nunca traz** a assinatura esperada nem o pedido canônico calculado pelo servidor. Para depurar, compare o seu canônico com os vetores. - **Dependência fora.** Se o armazenamento de nonces, o cofre de segredos ou o limitador estiverem fora, o servidor responde `503 service_unavailable` com `Retry-After: 1`. Nunca aceita "sem checar". Repita com backoff. - **Rota inexistente.** A autenticação vem antes do roteamento: uma rota errada com assinatura errada devolve `401`, não `404`. #### Escopos Cada chave tem de um a quatro escopos. A regra é *deny-by-default*: método ou rota não mapeados voltam `403 insufficient_scope`. | Rota | Escopo exigido | |---|---| | `GET /v1/ping` | nenhum (qualquer chave válida) | | Qualquer outro `GET` ou `HEAD`, inclusive `GET /sandbox/webhooks` | `read` | | `POST /deposits`, `POST /deposits/{id}/cancel` | `deposits:write` | | `POST /sandbox/deposits/{id}/pay`, `…/expire`, `…/refund`, `…/chargeback` e `POST /sandbox/bank-transfers/incoming` | `deposits:write` | | `POST /withdrawals`, `POST /withdrawals/{id}/cancel`, `POST /withdrawals/{id}/recall` | `withdrawals:write` | | `POST /sandbox/withdrawals/{id}/{ação}` | `withdrawals:write` | | `POST /rules/change-requests` | `rules:propose` | | `POST /fx/quotes`, `POST /sandbox/outage`, `POST /sandbox/webhooks/redeliver` | `deposits:write` **ou** `withdrawals:write` | O painel mostra os escopos com rótulos em português. A criação de chave marca `read` por padrão: | Rótulo no painel | Escopo | |---|---| | Ler dados | `read` | | Criar depósitos | `deposits:write` | | Pedir saques | `withdrawals:write` | | Propor mudança de regra | `rules:propose` | **Escopos mínimos por componente.** Duas chaves com escopos menores são melhores que uma chave com tudo: | Componente do conector | Escopos | |---|---| | Serviço que cria e cancela depósitos | `deposits:write` e `read` (o fetch-back é `GET`) | | Serviço que pede saques e paga | `withdrawals:write` e `read` | | Job de eventos, conciliação e saldos | `read` | | Ferramenta interna de propor regras | `rules:propose` (e `read`, para o `GET /rules` que dá o `ETag`) | A criação, a revelação do segredo (uma única vez), a revogação e a troca de chaves são feitas no painel, com aprovação de outra pessoa: veja [Chaves de API](../painel/chaves-de-api.md). Uma operação tem no máximo **2 chaves ativas**, o que permite trocar sem parada: crie a nova, faça o deploy do conector, confira o "último uso" no painel e revogue a antiga. A revogação vale em até 60 s em todos os nós. ### Exemplos Ping assinado, com `curl` e as funções de [`exemplos/shell/lib.sh`](../exemplos/shell/lib.sh). O host é o da sua operação ([Ambientes, tipos de operação e hosts](../conceitos/ambientes-e-hosts.md#hosts)): ```bash source exemplos/shell/lib.sh ts="$(date +%s)"; nonce="$(openssl rand -hex 16)" sig="$(dracma_assinar "$(cat "$DRACMA_KEY_SECRET_FILE")" "$ts" "$nonce" GET /v1/ping)" curl -sS --path-as-is --max-time 10 \ -H "Dracma-Key-Id: $DRACMA_KEY_ID" -H "Dracma-Timestamp: $ts" \ -H "Dracma-Nonce: $nonce" -H "Dracma-Signature: $sig" \ "$DRACMA_BASE_URL/ping" ``` `DRACMA_BASE_URL` termina em `/v1`. O segredo fica em arquivo com permissão `0400`, fora do repositório; o `lib.sh` é para depurar, e em produção você usa a biblioteca do seu cliente. ### Erros e casos de borda | Resposta | Causa mais provável | O que fazer | |---|---|---| | `401 invalid_signature` com o canônico igual ao dos vetores | `Dracma-Key-Id` digitado errado, segredo de outra chave, segredo com espaço ou quebra de linha no fim | Releia o segredo do cofre sem `\n`; confira o `key_id` | | `401 invalid_signature` só em `POST` | Corpo reserializado depois de assinado, ou `Content-Length` diferente do corpo assinado | Assine e envie os mesmos bytes | | `401 invalid_signature` só com query | Query reordenada ou decodificada pelo cliente HTTP | Assine a URL exatamente como enviada | | `401 timestamp_out_of_window` | Relógio fora de ±300 s; timestamp em milissegundos; com sinal, espaço ou zero à esquerda | NTP; `floor(agora / 1000)` | | `401 replayed_nonce` | Nonce reutilizado, inclusive em *retry* | Gere nonce novo a cada tentativa | | `401 unknown_key` | Prefixo `key_test_` ou `key_live_` do outro tipo de operação; chave revogada; `key_id` mal formado | Confira a chave e o host | | `403 insufficient_scope` | Chave sem o escopo da rota; método fora de `GET`, `HEAD`, `POST` | Use a chave do componente certo | | `403 operation_suspended` | Operação suspensa pelo Dracma | [Perguntas frequentes](../ajuda/faq.md#operacao-suspensa-403-operation_suspended) | | `429 rate_limited` | Limite de taxa ou 20 falhas de assinatura por minuto do mesmo IP | [Limites e quotas](limites.md#baldes-de-taxa) | | `503 service_unavailable` | Dependência do Dracma fora (nonces, segredos, limitador) | Espere `Retry-After` e repita | Roteiro de diagnóstico completo, com os passos na ordem, em [Problemas de integração](../ajuda/troubleshooting.md). ### Veja também - [Convenções HTTP](../conceitos/convencoes-http.md) - [Erros](erros.md) - [Idempotência e repetição](idempotencia.md) - [Limites e quotas](limites.md) - [Chaves de API](../painel/chaves-de-api.md) - [Rede, lista de IPs e mTLS](../seguranca/rede-allowlist-e-mtls.md) - [Segredos e credenciais](../seguranca/segredos-e-credenciais.md) - [Problemas de integração](../ajuda/troubleshooting.md) --- # Arquivo: docs/api/erros.md ## Erros > Para: Dev — Conferido em 2026-10-18. Todo erro da API `/v1` é um objeto `application/problem+json` com um `code` estável. Esta página lista o formato do erro, os 30 `code` que a API emite hoje, os 22 motivos de validação (`errors[].reason`), as respostas da borda que não têm corpo e o que acontece quando uma dependência do Dracma está fora. ### O que é O corpo do erro segue a RFC 9457. O campo que o seu código testa é o `code`. ```json { "type": "https://docs.dracma.test/errors/insufficient_scope", "title": "insufficient_scope", "status": 403, "code": "insufficient_scope", "trace_id": "4bf92f3577b34da6a3ce929d0e0e4736", "retryable": false } ``` | Campo | Presença | Uso | |---|---|---| | `code` | sempre | **Estável.** É o que o seu código testa. Códigos novos só se acrescentam. | | `status` | sempre | O mesmo status HTTP da resposta. | | `retryable` | sempre | `true` se vale repetir **com a mesma `Idempotency-Key`**. | | `trace_id` | sempre | Também no cabeçalho `Dracma-Trace-Id`. Guarde no log; é o que o suporte pede. | | `type` | sempre | Identificador opaco (`https://docs.dracma.test/errors/`). Não é uma página: o domínio não resolve. | | `title` | sempre | Igual ao `code`. | | `detail` | **omitido quando vazio** | Aparece só em poucos erros (`internal_error`, `day_open`, ação de sandbox desconhecida). Nunca faça *parse* dele. | | `errors[]` | em `validation_failed` | Itens com `field`, `message` e `reason` (o `reason` é omitido se vazio). | | `existing` | em `409 duplicate_external_id` | O depósito ou saque que já existe com esse `external_id`. | O Dracma nunca devolve rastro de pilha, SQL, segredo, a assinatura esperada nem o pedido canônico calculado. ### Quando usar Use o `code` para decidir o fluxo do conector e o `retryable` (junto da classe HTTP) para decidir se repete. Use o `trace_id` para falar com o suporte. Use `errors[].field` e `errors[].reason` para corrigir o seu pedido, e nunca para mostrar texto ao jogador: `message` não é contrato. ### Como funciona #### O que fazer com cada resposta | Resposta | Significado | Ação | |---|---|---| | `2xx` | Feito (ou já estava feito) | Grave o resultado. | | `4xx`, exceto `409 idempotency_in_progress`, `409 day_open` e `429` | O Dracma **decidiu** recusar | Não repita igual. Corrija a causa ou encerre o fluxo. | | `409 idempotency_in_progress`, `409 day_open`, `429`, `5xx` | Sem efeito conclusivo, ou ainda cedo | Espere o `Retry-After` e repita com a **mesma** `Idempotency-Key`. | | *timeout*, conexão caiu | Você **não sabe** se houve efeito | Repita com a mesma chave; ao desistir, consulte por `external_id` ([Idempotência e repetição](idempotencia.md#recuperar-um-post-perdido)). | | `code` que você não conhece | Código novo (a lista só cresce) | Trate pela classe HTTP e pelo `retryable`. | #### Catálogo de `code` emitidos São **30 códigos**. `Repetir` = repetir com a mesma `Idempotency-Key` (`retryable: true`). | HTTP | `code` | Repetir | Quando | O que fazer | |---|---|---|---|---| | 400 | `validation_failed` | não | Corpo, parâmetro ou cabeçalho inválido (`errors[]`); `Idempotency-Key` ausente; 4xx genérico do servidor HTTP | Corrija o pedido. É erro do seu lado. | | 400 | `currency_not_enabled` | não | Moeda não habilitada na operação | Ajuste no onboarding; confira `GET /currencies`. | | 400 | `method_not_enabled` | não | Método não habilitado para a moeda (hoje só `pix` em BRL) | Não ofereça o método. | | 400 | `rail_not_enabled` | não | `POST /sandbox/bank-transfers/incoming`: nenhum trilho bancário está habilitado | Não use a rota. | | 401 | `invalid_signature` | não | Assinatura não confere, chave inexistente, nonce mal formado, cabeçalho ausente | [Autenticação HMAC](autenticacao.md#ordem-de-verificacao). | | 401 | `timestamp_out_of_window` | não | Fora de ±300 s, ou timestamp fora da forma canônica | Corrija o relógio; segundos. | | 401 | `replayed_nonce` | não | Nonce já usado com essa chave | Gere nonce novo a cada tentativa. | | 401 | `unknown_key` | não | `key_id` mal formado, de outro tipo de operação, ou chave revogada | Confira a chave e o host. | | 403 | `ip_not_allowed` | não | IP de origem fora da lista da operação | A lista não é aplicada hoje; você não deve ver este código. | | 403 | `insufficient_scope` | não | A chave não tem o escopo da rota, ou o método não existe | Use a chave certa. | | 403 | `operation_suspended` | não | Operação suspensa pelo Dracma | [Perguntas frequentes](../ajuda/faq.md#operacao-suspensa-403-operation_suspended). | | 404 | `not_found` | não | O objeto não existe **nesta operação** (inclusive de outra operação, nunca `403`); rota inexistente; `GET` em rota só `POST` | Confira o id e a chave. | | 409 | `idempotency_key_reused` | não | Mesma chave com pedido diferente | Bug do seu lado. | | 409 | `idempotency_in_progress` | sim | A mesma chave ainda executa (`Retry-After: 1`) | Espere e repita. | | 409 | `duplicate_external_id` | não | `external_id` já usado com outra chave | Leia `existing` e concilie. | | 409 | `invalid_state_transition` | não | Ação que o estado atual não permite (cancelar saque já enviado, cancelar depósito pago, `recall` de saque PIX) | Consulte o estado atual. | | 409 | `day_open` | sim | `GET /reconciliation` de um dia que ainda não fechou; `detail` traz o instante de abertura; `Retry-After` em segundos | Agende após o instante; não repita em laço. | | 412 | `etag_mismatch` | não | `If-Match` desatualizado em `POST /rules/change-requests` | Releia `GET /rules` e reenvie. | | 413 | `validation_failed` | não | Corpo acima de 64 KiB (`errors[0] = {field: "body", reason: "too_large"}`) | Reduza o corpo. | | 422 | `amount_below_minimum`, `amount_above_maximum` | não | Limite por transação | Mostre o limite ao jogador. | | 422 | `document_invalid` | não | Documento do jogador inválido (CPF/CNPJ, dígitos verificadores) | Revise o cadastro do jogador. | | 422 | `document_mismatch` | não | Documento diferente do já fixado para esse `external_ref` | Revise o cadastro do jogador. | | 422 | `player_blocked`, `destination_blocked` | não | Listas de bloqueio (mensagem genérica de propósito) | Não revele o motivo ao jogador. | | 422 | `velocity_exceeded` | não | Limite de frequência ou volume | Mostre "tente mais tarde". | | 422 | `insufficient_operation_balance` | não | Só saque: sem caixa livre na operação | Alerte o financeiro; nova tentativa é operação nova (`external_id` novo). | | 422 | `fx_not_enabled` | não | `POST /fx/quotes`: câmbio desligado em toda operação | Não use câmbio. | | 429 | `rate_limited` | sim | Limite de taxa (`Retry-After`) | [Limites e quotas](limites.md#baldes-de-taxa). | | 500 | `internal_error` | sim | Erro interno (`detail`: "erro interno; informe o trace_id") | Repita com a mesma chave; se persistir, abra chamado com o `trace_id`. | | 503 | `service_unavailable` | sim | Dependência do Dracma fora, ou queda simulada em operação Teste | Espere o `Retry-After` e repita. | Os motivos exatos dos `422` de dinheiro (limites, listas, frequência) dependem das regras da operação: consulte `GET /v1/rules` e [Decisão, limites e regras](../dinheiro/decisao-limites-e-regras.md#recusas-sincronas-422). !!! note "Reservado no contrato" Sete códigos do contrato 1.1.0 existem no catálogo mas a API **não os emite hoje**: `psp_unavailable`, `outside_operating_hours`, `fx_quote_expired`, `fx_quote_mismatch`, `corridor_not_allowed`, `beneficiary_bank_unreachable` e `recall_window_expired`. Quando um PSP está fora você recebe `service_unavailable`. Trate-os como qualquer `code` desconhecido: pela classe HTTP e pelo `retryable`. #### Motivos de validação: `errors[].reason` Em `400 validation_failed`, cada item de `errors[]` traz o `field` (por exemplo `player.document.number`; para cabeçalhos, o nome do cabeçalho; para o corpo inteiro, `body`) e um `reason`. A lista é aberta: trate um `reason` desconhecido como erro de validação genérico. São 22 motivos emitidos hoje: | `reason` | Quando | |---|---| | `unknown_field` | Campo fora do schema (inclusive `status`, `version`, `paid_amount`, `decision`, `operation_id` aninhado) | | `duplicate_key` | Chave JSON repetida em qualquer nível, inclusive variando a caixa | | `invalid_utf8` | Corpo com UTF-8 inválido | | `syntax` | JSON malformado | | `trailing_data` | Conteúdo depois do objeto JSON | | `type` | Tipo errado (número onde vai string, por exemplo) | | `invalid` | Corpo inválido por outro motivo (por exemplo `amount` fora do formato de `Money`) | | `invalid_json` | Corpo de `POST /fx/quotes` que não é JSON válido | | `required` | Campo obrigatório ausente; também `Idempotency-Key` ausente ou fora do formato | | `format` | Formato inválido: `external_id`, `external_ref`, documento, país, IP, e-mail, `updated_since`, `cursor`, `after` | | `enum` | Valor fora do enum: `method`, `player.document.type`, `destination.pix.key_type`, `status` de saque | | `range` | Faixa: `expires_in_seconds`, `metadata` com mais de 20 chaves, `limit` fora de 1 a 200, `name`/`device_id` longos no saque | | `one_of` | `destination` com zero ou mais de um método | | `method_mismatch` | O método do `destination` difere de `method` | | `currency_mismatch` | `paid_amount.currency` diferente da moeda do depósito em `/sandbox/deposits/{id}/pay` | | `unknown` | `after` de `GET /events` que não é um evento da sua operação | | `too_large` | Corpo acima de 64 KiB (status `413`) | | `required_by_operation` | Campo que a operação exige e o contrato deixa opcional (por exemplo `player.email`) | | `max_length` | `player.email` com mais de 254 caracteres, `player.name` com mais de 120 no depósito; `justification` do `recall` com mais de 1000 | | `reserved_domain_required` | Em operação Teste, `player.email` que não é de domínio reservado (`.test`, `.invalid`, `.example`) | | `rail_not_enabled` | Também aparece como motivo em `POST /sandbox/bank-transfers/incoming` | | `length` | `justification` de `POST /rules/change-requests` fora de 1 a 1000 caracteres | `status` com valor desconhecido e `limit` fora da faixa aparecem como `format` em `GET /deposits` e como `enum` e `range` em `GET /withdrawals`; não dependa da diferença. A resposta de validação de `GET /events`, `/reconciliation`, `/balances`, `/rules` e `/players/{ref}/limits` traz `message` fixa ("parâmetro inválido"). Exemplo de campo desconhecido no corpo (o servidor recusa `additionalProperties`): ```json { "type": "https://docs.dracma.test/errors/validation_failed", "title": "validation_failed", "status": 400, "code": "validation_failed", "trace_id": "4bf92f3577b34da6a3ce929d0e0e4736", "retryable": false, "errors": [ { "field": "paid_amount", "message": "campo não permitido", "reason": "unknown_field" } ] } ``` #### Respostas da borda (sem `problem+json`) Antes de a requisição chegar à API, a entrada pública do Dracma pode responder sozinha. Essas respostas **não** têm corpo `problem+json`: | Resposta | Quando | |---|---| | `404` sem corpo | Qualquer caminho fora de `/v1/*` (`/health`, `/metrics`, `/`); `/v1/sandbox/*` em operação Real (nunca existe) | | `413` sem corpo | Corpo acima de 1 MB (a API só chega a avaliar até 64 KiB) | Falhas de TLS ou de rede também não têm corpo. O conector deve tratar uma resposta que não é JSON como falha de transporte: repita com a mesma `Idempotency-Key`. #### Rotas, métodos e erros genéricos - **Rota inexistente** em `/v1` com credencial válida: `404 not_found`. A autenticação vem antes: com assinatura inválida a resposta é `401`. - **Método errado.** Um `GET` em uma rota que só aceita `POST` volta `404 not_found`. Qualquer método além de `GET`, `HEAD` e `POST`, ou um `POST` em rota que não existe, volta `403 insufficient_scope`, porque o escopo é conferido antes da rota. - **Erro genérico do servidor HTTP** (caminho malformado, cabeçalho inválido, 4xx que não é do Dracma) vira `400 validation_failed`; o corpo não traz `errors[]`. - **Erro inesperado** vira `500 internal_error`, sempre opaco. O motivo real fica no log do Dracma, ligado ao `trace_id`. #### Modo degradado O Dracma não "aceita sem checar": se uma dependência está fora, a rota recusa ou desvia para revisão. A tabela mostra o que você vê. | Dependência fora | `POST /deposits` | `POST /withdrawals` | Leituras | |---|---|---|---| | Motor de regras | `503 service_unavailable`, nenhum depósito criado | `201` com o saque em `under_review` e `RULES_UNAVAILABLE` em `decision.reasons` (revisão manual) | `GET /rules` e `GET /players/{ref}/limits`: `503` | | Livro de saldos | segue | segue; o motor pode registrar `BALANCE_UNAVAILABLE` e mandar para revisão, nunca liberar | `GET /balances` e `GET /reconciliation`: `503` | | Serviço de webhooks | segue; os eventos esperam e são entregues depois | segue | `GET /reconciliation`, `GET /sandbox/webhooks` e `redeliver`: `503`; `GET /events` continua | | Executor de pagamentos | `202` com o depósito em `created` até a cobrança sair; acompanhe por webhook ou `GET /deposits/{id}` | `201`; o envio ao banco é retomado depois | ações de sandbox: `503` com `Retry-After: 5` | | Armazenamento de nonces, cofre de segredos, limitador, idempotência | `503` com `Retry-After: 1`, antes de qualquer efeito | idem | idem | Em todos os `503` a regra é a mesma: nenhum efeito parcial; espere o `Retry-After` e repita com a **mesma** `Idempotency-Key`. Um saque em `under_review` por `RULES_UNAVAILABLE` não é erro: veja [Decisão, limites e regras](../dinheiro/decisao-limites-e-regras.md). ### Exemplos Recusa por limite de taxa. Repare em `retryable: true` e no `Retry-After`: ```text HTTP/1.1 429 Too Many Requests Content-Type: application/problem+json Retry-After: 2 Dracma-Trace-Id: 4bf92f3577b34da6a3ce929d0e0e4736 ``` ```json { "type": "https://docs.dracma.test/errors/rate_limited", "title": "rate_limited", "status": 429, "code": "rate_limited", "trace_id": "4bf92f3577b34da6a3ce929d0e0e4736", "retryable": true } ``` Um tratador mínimo, em pseudocódigo: ```text resposta = enviar(pedido) se resposta.status é 2xx: gravar e seguir se resposta.json.retryable ou status é 5xx ou 429 ou erro de rede: esperar Retry-After (ou backoff com jitter) e repetir com a MESMA Idempotency-Key senão: registrar code, errors[] e trace_id; corrigir ou encerrar o fluxo ``` ### Erros e casos de borda - **Recusa de negócio não cria objeto.** Um `4xx` em `POST /deposits` ou `POST /withdrawals` não cria depósito nem saque. No saque, devolva a reserva ao jogador ao receber um `4xx` conclusivo, e só então. A mesma chave devolve a mesma recusa por 30 dias; uma nova tentativa é uma operação nova, com `external_id` e chave novos. - **Erro de validação também fica gravado.** Um `400` por corpo inválido é guardado com a chave; corrigir o corpo exige uma chave nova (mesmo corpo com a chave antiga devolve o mesmo `400`). - **`409 duplicate_external_id`** traz o objeto existente em `existing`: use-o para conciliar em vez de criar outro. - **Motivos de revisão não são erros.** Quando um saque é aceito mas vai para `under_review`, os motivos vêm em `decision.reasons`, não em `errors[]`. ### Veja também - [Idempotência e repetição](idempotencia.md) - [Autenticação HMAC](autenticacao.md) - [Limites e quotas](limites.md) - [Convenções HTTP](../conceitos/convencoes-http.md) - [Problemas de integração](../ajuda/troubleshooting.md) - [Rotas e campos reservados](../referencia/superficies-reservadas.md) - [Decisão, limites e regras](../dinheiro/decisao-limites-e-regras.md) --- # Arquivo: docs/api/idempotencia.md ## Idempotência e repetição > Para: Dev — Conferido em 2026-10-18. Dinheiro não admite "talvez tenha ido". Esta página diz, para cada resposta possível, se você repete e como, e garante que uma repetição **nunca** cria um segundo depósito ou um segundo saque. ### O que é A **regra de ouro**: > **4xx não se repete** (é uma decisão do Dracma). **5xx, 429 e erro de rede se repetem com a MESMA > `Idempotency-Key`** até uma resposta conclusiva. **Nunca gere chave nova para "tentar de novo" um `POST` de dinheiro.** Dois mecanismos trabalham juntos: | Mecanismo | Quem cria | Protege contra | |---|---|---| | `Idempotency-Key` (cabeçalho) | Você, a cada operação de escrita | A mesma requisição executar duas vezes (rede que cai, *timeout*, *retry*) | | `external_id` (campo do corpo) | Você, a cada depósito ou saque | Dois objetos para a mesma operação do seu lado, mesmo com chaves diferentes | ### Quando usar `Idempotency-Key` é **obrigatório em todo `POST`**: criar depósito, criar saque, `cancel`, `recall`, propor mudança de regras, cotação e todas as rotas `/sandbox/*`. `GET` e `HEAD` não usam chave: são sempre seguros de repetir. Sem a chave, ou com ela fora do formato, a resposta é `400 validation_failed` com `errors[0] = {field: "Idempotency-Key", reason: "required"}`, antes de qualquer efeito. ### Como funciona #### A chave - **Formato:** 16 a 128 caracteres `[A-Za-z0-9_.:-]`. - **Escopo:** por operação. A mesma chave vale para um único pedido nessa operação, em qualquer rota. - **Retenção:** a resposta fica guardada por **30 dias**. Depois disso a chave pode ser reusada, mas o `external_id` continua único (`409 duplicate_external_id`). - **Pedido inteiro:** a chave fica ligada ao método, ao caminho com query e ao corpo cru. Qualquer diferença (um espaço, outra ordem de campos) é um pedido diferente. | Situação | Resposta | |---|---| | Mesma chave e mesmo pedido, primeira execução já terminou em `2xx` ou `4xx` | **A mesma resposta** (status e corpo originais), com o cabeçalho `Idempotent-Replayed: true` | | Mesma chave e pedido diferente (outro corpo, outra rota, outra query) | `409 idempotency_key_reused`. Não é repetível: é um bug do seu lado | | Mesma chave enquanto a primeira ainda executa | `409 idempotency_in_progress` com `Retry-After: 1`. Repetível | | Primeira execução terminou em `5xx` ou `429` | A chave é **liberada**: repetir com a mesma chave executa de novo, sem duplicar | | A execução anterior ficou presa (o processo do Dracma caiu no meio) | A reserva da chave vale **30 s**; depois disso a repetição executa | Três consequências que costumam surpreender: 1. **A resposta repetida é um retrato, não o estado atual.** O *replay* devolve o corpo da primeira execução. Para o estado de agora, faça `GET /deposits/{id}` ou `GET /withdrawals/{id}`. 2. **Uma recusa `4xx` fica gravada.** Repetir a mesma chave devolve a mesma recusa por 30 dias, inclusive um `400` de validação. Para tentar de novo depois de uma recusa (o jogador corrigiu o documento, você corrigiu o corpo) é uma **operação nova**: `external_id` novo **e** chave nova. 3. **O nonce não é a chave.** Cada tentativa tem novo timestamp, novo nonce e nova assinatura, e a mesma `Idempotency-Key` ([Autenticação HMAC](autenticacao.md)). #### Convenção recomendada: chave igual ao `external_id` Em `POST /deposits` e `POST /withdrawals`, use o `external_id` como `Idempotency-Key`, por exemplo `dep_` e `wdr_` (o UUIDv7 dá ordem temporal e cabe no formato). Vantagens: - a chave sobrevive a um *restart* do seu processo: ela está no seu banco, junto do registro do depósito; - não existe "gerar chave nova sem querer": um `external_id` novo é uma operação nova e consciente; - o suporte acha tudo pelo mesmo ID. Para ações sobre um objeto (`cancel`), derive a chave de forma determinística, por exemplo `cnl_`. Grave o `external_id` e a chave **antes** de chamar o Dracma (padrão *outbox*): se o processo morrer no meio, o *worker* que retoma usa a mesma chave. #### `external_id` - **Único por operação e por tipo:** depósito e saque têm espaços separados. De 1 a 64 caracteres `[A-Za-z0-9_.:-]`. - **Duplicado:** um segundo `POST` com o mesmo `external_id` e **outra** chave volta `409 duplicate_external_id`, com o objeto já existente em `existing`. Use-o para conciliar em vez de criar outro. - **Liga o webhook ao seu registro:** no fetch-back, confira que o `external_id` devolvido é um que **você** criou ([Receptor de webhooks](../webhooks/receptor.md)). #### Recuperar um `POST` perdido Se o *timeout* estourou e você desistiu de repetir (por exemplo, o processo reiniciou sem a chave), **consulte antes de decidir**: ```text GET /v1/deposits?external_id= GET /v1/withdrawals?external_id= ``` - Lista vazia (`"data": []`): o Dracma não criou o objeto. Você pode repetir o `POST` com a mesma chave (ou uma nova, se a antiga passou de 30 dias). - Um item: o objeto existe. Siga pelo estado dele; não crie outro. `GET /deposits/{id}` e `GET /withdrawals/{id}` aceitam só ids no formato do Dracma (`dep_…`, `wdr_…`). Um `id` fora desse formato volta `404 not_found`. A consulta por `external_id` conta no limite de listagem ([Limites e quotas](limites.md#baldes-de-taxa)): use-a para recuperação, e o `GET` por `id` para o fetch-back de rotina. Por que isso importa no saque: se o `POST /withdrawals` deu *timeout* e você devolve a reserva ao jogador sem saber o resultado, e o saque na verdade foi aceito, o jogador recebe o PIX **e** o saldo de volta. Enquanto não houver resposta conclusiva, a reserva fica onde está. #### Backoff Backoff exponencial com *jitter* (por exemplo 1 s, 2 s, 4 s, até 60 s), respeitando o `Retry-After` quando vier. O `Retry-After` do `429` é calculado pelo balde de limite; o do `503` é de 1 s (5 s em ações de sandbox). Defina um teto de tentativas e um destino para quando ele estourar (a fila de exceções do seu conector). #### Árvore de decisão ```mermaid flowchart TD A[Gravou external_id + Idempotency-Key no seu banco] --> B[POST assinado] B -->|2xx| C[Grave o objeto; siga por webhook + fetch-back] B -->|Idempotent-Replayed: true| C B -->|4xx conclusivo| D[Encerre: não cria objeto. Saque: devolva a reserva] B -->|409 idempotency_in_progress, 429, 5xx| E[Espere Retry-After ou backoff] B -->|timeout ou erro de rede| E E --> F{Tentativas esgotadas?} F -->|não| B F -->|sim| G[Não decida às cegas: GET ?external_id= e marque para revisão + alerta] ``` ### Exemplos Primeira tentativa que deu *timeout*, seguida de repetição com a mesma chave. A segunda chamada traz **novo** nonce e nova assinatura: ```text POST /v1/withdrawals Idempotency-Key: wdr_0192a3b4-5c6d-7e8f-9a0b-1c2d3e4f5a6b Dracma-Nonce: 0123456789abcdef0123456789abcdef → (sem resposta: timeout de 10 s) POST /v1/withdrawals (mesmo corpo, byte a byte) Idempotency-Key: wdr_0192a3b4-5c6d-7e8f-9a0b-1c2d3e4f5a6b Dracma-Nonce: fedcba9876543210fedcba9876543210 → 201 Created Idempotent-Replayed: true (a primeira tentativa tinha sido executada) ``` Se o `Idempotent-Replayed: true` não vem na segunda resposta, a primeira não chegou a ser executada e esta foi a execução real. Nos dois casos o resultado é o mesmo: um único saque. ### Erros e casos de borda | Situação | O que acontece | O que fazer | |---|---|---| | Chave com menos de 16 caracteres | `400 validation_failed` (`Idempotency-Key`, `required`) | Use uma chave de 16 a 128 caracteres | | Mesma chave, corpo reserializado com outra ordem de campos | `409 idempotency_key_reused` | Guarde os bytes do corpo na primeira tentativa e reenvie esses bytes | | Duas instâncias do conector enviam a mesma chave ao mesmo tempo | Uma executa; a outra recebe `409 idempotency_in_progress` | Espere o `Retry-After` e repita | | `4xx` de negócio (`422 player_blocked`) e repetição com a mesma chave | A mesma recusa, por 30 dias | Não repita; é uma decisão | | `429` ou `5xx` e repetição com a mesma chave | Executa de novo, sem duplicar | Repita com backoff | | Chave de mais de 30 dias | A resposta guardada foi apagada | Prefira o `external_id` (continua único) e consulte por ele | | `409 duplicate_external_id` | O objeto já existe | Leia `existing`; não crie outro | ### Veja também - [Erros](erros.md) - [Autenticação HMAC](autenticacao.md) - [Limites e quotas](limites.md) - [Convenções HTTP](../conceitos/convencoes-http.md) - [Receptor de webhooks](../webhooks/receptor.md) - [Ciclo do saque](../dinheiro/ciclo-do-saque.md) --- # Arquivo: docs/api/paginacao.md ## Paginação e filtros > Para: Dev — Conferido em 2026-10-18. Toda listagem da API devolve `{"data": [...], "next_cursor": ...}`. Esta página mostra, endpoint por endpoint, o nome do parâmetro de cursor, em que ordem os itens saem, quais filtros existem e como percorrer uma lista inteira sem pular nem repetir itens. ### O que é A paginação é por **cursor**, nunca por número de página. Cada resposta traz até `limit` itens e um `next_cursor`: - `next_cursor` com valor: existe pelo menos mais um item; mande esse valor no parâmetro de cursor da próxima chamada; - `next_cursor` igual a `null`: acabou. A API só devolve cursor quando há, de fato, mais um item, então não existe "última página vazia". | Parâmetro | Regra | |---|---| | `limit` | Inteiro de **1 a 200**. Padrão **50**. Fora da faixa, `400 validation_failed`. | | cursor | Texto devolvido em `next_cursor`. O nome do parâmetro muda por endpoint (tabela abaixo). Cursor inválido: `400 validation_failed` com `reason: format`. | ### Quando usar - **Recuperar um `POST` perdido:** `GET /deposits?external_id=…` ou `GET /withdrawals?external_id=…` (uma página basta). - **Varrer um período:** `updated_since` com `limit=200`, para conciliação ou para reconstruir um estado perdido. - **Seguir o que aconteceu:** `GET /events` com o cursor `after`, que é a ferramenta de recuperação de webhooks ([Recuperação por `GET /events`](../webhooks/recuperacao-get-events.md)). - **Estado de rotina de um objeto:** não liste. Use `GET /deposits/{id}` ou `GET /withdrawals/{id}`, que têm balde de limite maior ([Limites e quotas](limites.md#baldes-de-taxa)). ### Como funciona #### Cursor, ordem e filtros por endpoint | Endpoint | Parâmetro do cursor | O que é o cursor | Ordem dos itens | Filtros | |---|---|---|---|---| | `GET /deposits` | `cursor` | o `id` (`dep_…`) do último item recebido | por `id`, que acompanha a ordem de criação | `external_id`, `player_ref`, `status`, `updated_since`, `limit` | | `GET /withdrawals` | `cursor` | texto opaco | por data de criação e, no empate, por `id` | `external_id`, `player_ref`, `status`, `updated_since`, `limit` | | `GET /events` | `after` | o `id` (`evt_…`) do último evento recebido | por criação do evento | `object_id`, `limit` | | `GET /reconciliation` | `cursor` | texto opaco | por `object_id` e, no empate, por `kind` | `date` e `currency` (obrigatórios), `limit` | | `GET /receipts` | `cursor` | nunca devolvido | sempre vazio | `status`, `updated_since`, `limit` | Pontos de atenção: - **Não ordena por `updated_at`.** `GET /deposits` sai em ordem de criação, e um depósito atualizado agora continua na posição em que nasceu. Para achar o que mudou, filtre por `updated_since`. - **Cursor opaco é opaco.** O cursor de `/withdrawals` e o de `/reconciliation` são texto que só o Dracma interpreta: não construa, não edite, não ordene e não guarde por mais tempo que a varredura. O formato pode mudar sem aviso. O cursor de `/deposits` e `/events` é um `id` que você já viu; mesmo assim, use sempre o valor de `next_cursor`. - **Cursor serve só para o mesmo conjunto de filtros.** Mudou um filtro no meio da varredura: recomece sem cursor. - **`GET /events` com `after` de outra operação ou que não existe** volta `400` com `reason: unknown` (a API não confirma se o evento existe em outra operação). - **`GET /receipts`** devolve sempre `{"data": [], "next_cursor": null}` e valida os parâmetros; qualquer `cursor` enviado volta `400` (`reason: format`), porque a API nunca emite cursor de recebimento ([Rotas e campos reservados](../referencia/superficies-reservadas.md#recebimentos-bancarios-get-v1receipts-e-get-v1receiptsid)). - **`GET /reconciliation`** devolve os itens do dia em páginas; os totais, o digest e a assinatura (cabeçalho `Dracma-Signature`) vêm na **última** página, a que tem `next_cursor` `null` ([Reconciliação](../referencia/reconciliacao.md#paginacao-totais-e-assinatura)). #### `updated_since` `updated_since` é um instante RFC 3339 (`2026-10-18T12:00:00Z`; aceita fração de segundo) e filtra por `updated_at >= updated_since`: **maior ou igual**, inclusive. Por isso o item exatamente na borda reaparece. Para uma varredura incremental: 1. guarde o maior `updated_at` que você viu (`T`) e a `version` de cada objeto; 2. na próxima rodada, chame com `updated_since=T`; 3. descarte o que você já tem com a mesma `version` (a `version` sobe a cada transição); 4. nunca avance `T` além do maior `updated_at` realmente lido. O mais robusto para acompanhar mudanças é o webhook mais o `GET /events`; `updated_since` é para conferência. #### Percorrer tudo ```text cursor = vazio repita: resposta = GET /v1/deposits?updated_since=T&limit=200 (+ &cursor= se houver) processe(resposta.data) se resposta.next_cursor é null: pare cursor = resposta.next_cursor espere pelo menos 200 ms entre páginas # a listagem tem balde próprio de 5 req/s ``` Cada página é uma requisição assinada nova (novo timestamp e nonce). O `Retry-After` do `429` diz quanto esperar; repita a **mesma** página, com o mesmo cursor. ### Exemplos Primeira página de saques, e a seguinte. O caminho assinado inclui a query exatamente como enviada ([Autenticação HMAC](autenticacao.md)): ```text GET /v1/withdrawals?status=processing&limit=2 → 200 {"data":[{...},{...}],"next_cursor":""} GET /v1/withdrawals?status=processing&limit=2&cursor= → 200 {"data":[{...}],"next_cursor":null} ``` Eventos a partir do último que você processou: ```text GET /v1/events?after=evt_<último id processado>&limit=100 → 200 {"data":[ ...eventos... ],"next_cursor":"evt_"} ``` Consulta de um depósito pelo seu `external_id` (recuperação de `POST` perdido): ```text GET /v1/deposits?external_id=dep_0192a3b4-5c6d-7e8f-9a0b-1c2d3e4f5a6b → 200 {"data":[{...}],"next_cursor":null} ``` ### Erros e casos de borda | Situação | Resposta | |---|---| | `limit=0`, `limit=201` ou `limit=abc` | `400 validation_failed` (`field: limit`) | | `cursor` que o Dracma nunca emitiu, ou adulterado | `400 validation_failed` (`field: cursor`, `reason: format`) | | `status` desconhecido | `400 validation_failed` (`field: status`) | | `updated_since` fora de RFC 3339 | `400 validation_failed` (`field: updated_since`, `reason: format`) | | `after` de `GET /events` que não existe nesta operação | `400 validation_failed` (`field: after`, `reason: unknown`) | | Filtro sem resultado | `200` com `"data": []` e `"next_cursor": null` | | Muitas páginas por segundo | `429 rate_limited`; a listagem tem balde próprio de **5 req/s** por chave | | Objeto criado durante a varredura | Pode ou não aparecer, conforme a ordem; a próxima rodada com `updated_since` o pega | Mudar o `limit` no meio da varredura é permitido: o cursor não depende dele. ### Veja também - [Limites e quotas](limites.md) - [Erros](erros.md) - [Idempotência e repetição](idempotencia.md) - [Recuperação por `GET /events`](../webhooks/recuperacao-get-events.md) - [Depósitos (API)](../referencia/depositos.md) - [Saques (API)](../referencia/saques.md) - [Reconciliação (API)](../referencia/reconciliacao.md) --- # Arquivo: docs/api/limites.md ## Limites e quotas > Para: Dev — Conferido em 2026-10-18. A API limita a taxa de chamadas em vários baldes (por IP, por chave, por operação, por jogador) e limita o tamanho e o formato de cada campo. Esta página lista todos os baldes, mostra como o *token bucket* se comporta e reúne os limites de tamanho num só lugar. ### O que é Cada balde é um **token bucket** com duas medidas: - **taxa:** quantas fichas voltam por segundo (ou por hora, no caso dos limites por jogador); - **rajada (*burst*):** quantas fichas o balde guarda, ou seja, quantas chamadas você pode fazer de uma vez com o balde cheio. Cada chamada gasta uma ficha de cada balde que se aplica a ela. Se algum balde está vazio, a resposta é `429 rate_limited` com `Retry-After` (em segundos, arredondado para cima, no mínimo 1) dizendo quando haverá ficha. Não é um "contador por segundo fixo": um balde de `50/s` com rajada `100` deixa passar 100 chamadas de uma vez e depois 50 por segundo de forma contínua. O limitador é compartilhado entre todos os servidores do Dracma, então o limite vale para o conjunto. Se ele próprio estiver indisponível, a resposta é `503 service_unavailable` com `Retry-After: 1`: o Dracma nunca aceita "sem limite" quando não consegue contar. Os valores abaixo são os mesmos em operações Teste e Real, e iguais para todas as operações. Não há perfil "menor" para Teste. ### Quando usar Use a tabela para dimensionar o conector: tamanho de *pool* de conexões, ritmo de varredura de listagens e fila de saques. Os limites por jogador (10 depósitos e 3 saques por hora) também orientam a tela do seu produto: vale mostrar "tente mais tarde" em vez de deixar o jogador repetir. ### Como funciona #### Baldes de taxa | Balde | Taxa | Rajada | Chamadas que gastam a ficha | |---|---|---|---| | IP de origem, **antes** da autenticação | 200/s | 200 | toda chamada `/v1` | | Chave, escrita | 50/s | 100 | `POST` | | Operação, escrita | 50/s | 100 | `POST` | | Chave, leitura | 100/s | 100 | `GET` e `HEAD` | | Operação, leitura | 200/s | 200 | `GET` e `HEAD` | | **Listagem**, por chave | 5/s | 5 | `GET /deposits`, `GET /withdrawals`, `GET /events` e `GET /receipts`, sem `id` | | **Depósitos criados por jogador** (`player.external_ref`) | 10/hora | 10 | `POST /deposits` | | **Saques pedidos por jogador** (`player.external_ref`) | 3/hora | 3 | `POST /withdrawals` | | **Falhas de assinatura**, por IP | 20/minuto | 20 | cada `401 invalid_signature` | Uma chamada de escrita gasta uma ficha do balde da **chave** e uma do balde da **operação**; uma listagem gasta as fichas de leitura (chave e operação) **e** a de listagem. Vale o balde mais restritivo. Consequências práticas: - **Fetch-back não é listagem.** `GET /deposits/{id}` e `GET /withdrawals/{id}` usam só os baldes de leitura (100/s por chave). Faça o fetch-back por `id`; use a listagem por `external_id` apenas para recuperar um `POST` perdido. - **A listagem é o gargalo.** Com 5/s e `limit=200`, o teto de varredura é de 1.000 itens por segundo. Pagine com pausa de 200 ms ([Paginação e filtros](paginacao.md#percorrer-tudo)). - **`GET /reconciliation` não está no balde de listagem.** Ele usa os baldes de leitura (100/s por chave). O contrato fala em 5 req/s para ele; a API hoje aplica o balde de leitura. Não conte com a folga: um relatório por dia e por moeda não precisa de mais. - **Falhas de assinatura.** Com 20 falhas por minuto do mesmo IP, a resposta deixa de ser `401` e vira `429 rate_limited`. Um conector com a chave errada, em laço, tranca o próprio IP; corrija a causa em vez de repetir. #### Limites por jogador Dois baldes protegem um jogador contra abuso, e cada um é contado **por jogador e por operação**: | Limite | Valor | Resposta ao estourar | |---|---|---| | Depósitos criados | 10 por hora | `429 rate_limited` | | Saques pedidos | 3 por hora | `429 rate_limited` | Como eles funcionam: - A ficha só é gasta depois que o corpo foi validado: um `400` **não** consome ficha. - Um *replay* de idempotência (mesma chave, mesma resposta) não chega ao balde: não consome ficha. - Cada ficha volta aos poucos: 1 ficha a cada 6 minutos no depósito e 1 a cada 20 minutos no saque. O `Retry-After` pode ser, por isso, de vários minutos. **Não repita em laço**: mostre ao jogador "tente mais tarde" e guarde o pedido. - Se o limitador estiver fora, a resposta é `503 service_unavailable` (nunca "sem limite"). Repita com a mesma chave. - O limite por jogador é uma proteção do Dracma e é anterior às regras da operação (limites de valor e frequência por janela), que respondem `422`. Veja [Decisão, limites e regras](../dinheiro/decisao-limites-e-regras.md#quais-limites-existem). #### Tamanhos e formatos | Item | Limite | |---|---| | Corpo da requisição | **64 KiB** na API (1 MB na entrada pública, que corta antes) | | `amount.amount` | até 38 dígitos, sem zeros à esquerda | | `limit` de listagem | 1 a 200 (padrão 50) | | `external_id` | 1 a 64 caracteres `[A-Za-z0-9_.:-]` | | `player.external_ref` | 1 a 64 caracteres `[A-Za-z0-9_.:-]` | | `Idempotency-Key` | 16 a 128 caracteres `[A-Za-z0-9_.:-]` | | `Dracma-Key-Id` | `key_test_` ou `key_live_` + 1 a 64 caracteres `[A-Za-z0-9]` | | `expires_in_seconds` (depósito) | 300 a 86.400 (padrão 1.800) | | `metadata` | até 20 chaves | | `player.name` | até 120 caracteres | | `player.email` | até 254 caracteres | | `player.device_id` | até 128 caracteres | | `player.document.number` | CPF e CNPJ: só dígitos, de 11 a 14; outros documentos: até 32 caracteres | | `destination.pix.key` | 1 a 140 caracteres | | `justification` de `POST /rules/change-requests` | 1 a 1.000 caracteres | | `justification` de `POST /withdrawals/{id}/recall` | até 1.000 caracteres | | `object_id` e `after` de `GET /events` | até 64 caracteres (`after` com prefixo `evt_`) | | Chaves de API ativas por operação | **2** | O contrato define até 256 caracteres por valor de `metadata`. A API hoje confere apenas o número de chaves e o tamanho total do corpo; respeite os 256 caracteres, que é o contrato. #### Prazos fixos | Prazo | Valor | |---|---| | Janela do `Dracma-Timestamp` | ±300 s | | Retenção do nonce | 600 s | | Retenção da resposta de idempotência | 30 dias | | Reserva de uma chave em execução | 30 s | | Orçamento de resposta de `POST /deposits` | cerca de 7 s, depois `202` | | Folga depois de `expires_at` antes de expirar o depósito | 2 minutos | | Tempo de espera do `pay` de sandbox | até 3 s | ### Exemplos Lote de 150 saques de uma só vez, com uma chave de escrita (50/s, rajada 100): ```text chamadas 1 a 100 → passam (o balde estava cheio) chamada 101 → 429 rate_limited, Retry-After: 1 depois de ~1 s → mais 50 chamadas passam, e assim por diante ``` Em vez de disparar, ponha os pedidos numa fila e consuma a 20 por segundo, bem abaixo do teto. Cada pedido volta a ser uma chamada assinada nova. Quarto saque de uma hora para o mesmo jogador: ```text POST /v1/withdrawals (player.external_ref = "u-1", 4º pedido na mesma hora) → 429 rate_limited, Retry-After: 1200 ``` ### Erros e casos de borda | Situação | Resposta | O que fazer | |---|---|---| | Balde de IP, chave, operação ou listagem vazio | `429 rate_limited` + `Retry-After` | Espere e repita com a **mesma** `Idempotency-Key`; o `429` libera a chave e nunca duplica | | Limite por jogador estourado | `429 rate_limited` + `Retry-After` longo | Mostre "tente mais tarde" ao jogador; não faça laço | | 20 falhas de assinatura por minuto do mesmo IP | `429` no lugar de `401` | Corrija a assinatura ou a chave | | Limitador indisponível | `503 service_unavailable` + `Retry-After: 1` | Espere e repita | | Corpo acima de 64 KiB | `413 validation_failed` | Reduza o corpo | | Corpo acima de 1 MB | `413` sem corpo `problem+json`, da borda | Reduza o corpo | Um laço de `429` sem espera piora o problema: respeite o `Retry-After`, use backoff com *jitter* e limite as tentativas ([Idempotência e repetição](idempotencia.md)). !!! note "Limites de regras" Valores de limites de **regras** (valor mínimo e máximo por transação, frequência, teto agregado) são configuração da operação e não estão nesta página. Consulte `GET /v1/rules` e `GET /v1/players/{ref}/limits` ([Regras (API)](../referencia/regras.md)). ### Veja também - [Erros](erros.md) - [Idempotência e repetição](idempotencia.md) - [Paginação e filtros](paginacao.md) - [Convenções HTTP](../conceitos/convencoes-http.md) - [Regras (API)](../referencia/regras.md) - [Decisão, limites e regras](../dinheiro/decisao-limites-e-regras.md) --- # Arquivo: docs/referencia/README.md ## Índice de endpoints > Para: Dev — Conferido em 2026-10-18. Esta página lista as 31 rotas que um operador chama: 30 em `/v1` mais o documento OpenAPI. Para cada uma você vê o método, o escopo da chave, o `operationId` do documento OpenAPI e a página que explica a rota em detalhe. Use-a para achar rápido onde uma rota está descrita, ou para ir do `operationId` de um cliente gerado até o texto. ### O que é A API pública do Dracma tem **30 rotas sob `/v1`**: 21 de negócio e leitura (o ping entra aqui) e 9 de simulação (sandbox). O documento OpenAPI é a 31ª rota e fica fora de `/v1`. | Grupo | Rotas | Para que serve | |---|---|---| | Saúde e cadastro | 2 | `ping` e `currencies`: testar a credencial e descobrir moedas e métodos da sua operação | | Depósitos | 4 | criar a cobrança PIX, consultar, listar e cancelar | | Saques | 5 | pedir o pagamento, consultar, listar, cancelar e `recall` (reservado) | | Jogadores e saldos | 2 | limites do jogador e saldos da operação | | Regras | 2 | ler o conjunto vigente e propor mudança | | Reconciliação e eventos | 2 | relatório diário assinado e recuperação de eventos | | Câmbio e recebimentos | 4 | rotas **reservadas**: nenhuma operação tem câmbio nem trilho bancário habilitado | | Sandbox | 9 | simulam pagamento, expiração, estorno, saída de saque, queda e reentrega; só existem em operação Teste | | OpenAPI | 1 | `GET /openapi.yaml` | Toda rota sob `/v1` exige as quatro credenciais de assinatura (veja [Autenticação](../api/autenticacao.md)), e todo `POST` exige `Idempotency-Key` (veja [Idempotência](../api/idempotencia.md)). O prefixo `/v1` faz parte do caminho assinado. ### Quando usar - Quer saber **qual escopo** a chave precisa para uma rota: veja a coluna "Escopo". - Gerou um cliente a partir do OpenAPI e precisa achar o texto de uma operação: procure o `operationId`. - Quer o contrato campo a campo de um objeto: vá a [Objetos e campos](schemas.md). ### Como funciona O escopo vem da chave. As regras são estas: 1. `GET` (e `HEAD`) exige o escopo `read`, exceto `GET /v1/ping`, que aceita qualquer chave válida. 2. `POST` de depósito (inclusive as ações de sandbox de depósito) exige `deposits:write`. 3. `POST` de saque (inclusive recall e as ações de sandbox de saque) exige `withdrawals:write`. 4. `POST /v1/rules/change-requests` exige `rules:propose`. 5. `POST /v1/fx/quotes`, `POST /v1/sandbox/outage` e `POST /v1/sandbox/webhooks/redeliver` aceitam `deposits:write` **ou** `withdrawals:write`. Em `POST /v1/fx/quotes`, o `purpose` do corpo decide qual dos dois vale. 6. Escopo ausente responde `403 insufficient_scope`. Método ou rota de escrita que não está nesta lista é negado. `GET /v1/deposits`, `GET /v1/withdrawals`, `GET /v1/events` e `GET /v1/receipts` (sem id) usam o balde de listagem do rate limit. Veja [Limites e quotas](../api/limites.md#baldes-de-taxa). #### As 31 rotas | # | Método e caminho | Escopo | `operationId` | Descrição | |---|---|---|---|---| | 1 | `GET /openapi.yaml` | nenhum | — | [OpenAPI](openapi.md) | | 2 | `GET /v1/ping` | qualquer chave válida | `ping` | [Ping e moedas](ping-e-moedas.md) | | 3 | `GET /v1/currencies` | `read` | `listarMoedas` | [Ping e moedas](ping-e-moedas.md) | | 4 | `POST /v1/deposits` | `deposits:write` | `criarDeposito` | [Depósitos](depositos.md) | | 5 | `GET /v1/deposits` | `read` | `listarDepositos` | [Depósitos](depositos.md) | | 6 | `GET /v1/deposits/{id}` | `read` | `obterDeposito` | [Depósitos](depositos.md) | | 7 | `POST /v1/deposits/{id}/cancel` | `deposits:write` | `cancelarDeposito` | [Depósitos](depositos.md) | | 8 | `POST /v1/withdrawals` | `withdrawals:write` | `criarSaque` | [Saques](saques.md) | | 9 | `GET /v1/withdrawals` | `read` | `listarSaques` | [Saques](saques.md) | | 10 | `GET /v1/withdrawals/{id}` | `read` | `obterSaque` | [Saques](saques.md) | | 11 | `POST /v1/withdrawals/{id}/cancel` | `withdrawals:write` | `cancelarSaque` | [Saques](saques.md) | | 12 | `POST /v1/withdrawals/{id}/recall` | `withdrawals:write` | `pedirRecallDeSaque` | [Rotas e campos reservados](superficies-reservadas.md) | | 13 | `POST /v1/fx/quotes` | `deposits:write` ou `withdrawals:write` | `criarCotacao` | [Rotas e campos reservados](superficies-reservadas.md) | | 14 | `GET /v1/fx/quotes/{id}` | `read` | `obterCotacao` | [Rotas e campos reservados](superficies-reservadas.md) | | 15 | `GET /v1/receipts` | `read` | `listarRecebimentos` | [Rotas e campos reservados](superficies-reservadas.md) | | 16 | `GET /v1/receipts/{id}` | `read` | `obterRecebimento` | [Rotas e campos reservados](superficies-reservadas.md) | | 17 | `GET /v1/players/{external_ref}/limits` | `read` | `limitesDoJogador` | [Limites do jogador e saldos](jogadores-e-saldos.md) | | 18 | `GET /v1/balances` | `read` | `saldosDaOperacao` | [Limites do jogador e saldos](jogadores-e-saldos.md) | | 19 | `GET /v1/rules` | `read` | `obterRegras` | [Regras (API)](regras.md) | | 20 | `POST /v1/rules/change-requests` | `rules:propose` | `pedirMudancaDeRegras` | [Regras (API)](regras.md) | | 21 | `GET /v1/reconciliation` | `read` | `reconciliacao` | [Reconciliação](reconciliacao.md) | | 22 | `GET /v1/events` | `read` | `listarEventos` | [Recuperação com `GET /events`](../webhooks/recuperacao-get-events.md) | | 23 | `POST /v1/sandbox/deposits/{id}/pay` | `deposits:write` | `sandboxPagarDeposito` | [Sandbox](../sandbox/README.md) | | 24 | `POST /v1/sandbox/deposits/{id}/expire` | `deposits:write` | `sandboxExpirarDeposito` | [Sandbox](../sandbox/README.md) | | 25 | `POST /v1/sandbox/deposits/{id}/refund` | `deposits:write` | `sandboxEstornarDeposito` | [Sandbox](../sandbox/README.md) | | 26 | `POST /v1/sandbox/deposits/{id}/chargeback` | `deposits:write` | `sandboxChargebackDeposito` | [Sandbox](../sandbox/README.md) | | 27 | `POST /v1/sandbox/withdrawals/{id}/{action}` | `withdrawals:write` | `sandboxAgirNoSaque` | [Sandbox](../sandbox/README.md) | | 28 | `POST /v1/sandbox/bank-transfers/incoming` | `deposits:write` | `sandboxTransferenciaRecebida` | [Rotas e campos reservados](superficies-reservadas.md) | | 29 | `POST /v1/sandbox/outage` | `deposits:write` ou `withdrawals:write` | `sandboxQueda` | [Sandbox](../sandbox/README.md) | | 30 | `GET /v1/sandbox/webhooks` | `read` | `sandboxWebhooksEnviados` | [Sandbox](../sandbox/README.md) | | 31 | `POST /v1/sandbox/webhooks/redeliver` | `deposits:write` ou `withdrawals:write` | `sandboxReentregar` | [Sandbox](../sandbox/README.md) | As rotas 23 a 31 respondem `404 not_found` em operação Real: a rota não existe para você, nunca devolve `403`. Isso vale mesmo que a chave tenha o escopo certo. !!! note "Reservado no contrato" As rotas 12 a 16 e 28 existem para manter o contrato 1.1.0 inteiro, mas nenhuma operação tem câmbio nem trilho bancário habilitado. Elas respondem de forma previsível, sem efeito. O comportamento exato de cada uma está em [Rotas e campos reservados](superficies-reservadas.md). ### Exemplos Do `operationId` à página: um cliente gerado com o método `obterDeposito` chama `GET /v1/deposits/{id}`, que está descrito em [Depósitos](depositos.md). Um cliente com `reconciliacao` chama `GET /v1/reconciliation`, descrito em [Reconciliação](reconciliacao.md). Da necessidade à rota: | Eu quero... | Rota | |---|---| | Saber se a credencial e o relógio estão certos | `GET /v1/ping` | | Saber quais moedas e métodos minha operação aceita | `GET /v1/currencies` | | Cobrar um jogador | `POST /v1/deposits` | | Confirmar um pagamento antes de creditar | `GET /v1/deposits/{id}` | | Pagar um jogador | `POST /v1/withdrawals` | | Descobrir o destino de um `POST` cuja resposta se perdeu | `GET /v1/deposits?external_id=` ou `GET /v1/withdrawals?external_id=` | | Ver quanto cabe no saque automático de um jogador | `GET /v1/players/{external_ref}/limits` | | Monitorar o caixa da operação | `GET /v1/balances` | | Provar que seu livro e o do Dracma batem | `GET /v1/reconciliation` | ### Erros e casos de borda - Rota que não existe, ou método que a rota não aceita, responde `404 not_found`. Um `PUT` em `/v1/deposits` é `404`, não `405`. - Caminhos fora de `/v1` (exceto o documento OpenAPI) não são públicos. - Rota de sandbox em operação Real responde `404 not_found`. - A lista completa de `code` e `reason` está em [Erros](../api/erros.md). ### Veja também - [Convenções HTTP](../conceitos/convencoes-http.md): `Money`, datas, IDs e cabeçalhos. - [Paginação e filtros](../api/paginacao.md) - [Objetos e campos](schemas.md) - [Divergências conhecidas](divergencias.md) --- # Arquivo: docs/referencia/ping-e-moedas.md ## Ping e moedas > Para: Dev — Conferido em 2026-10-18. Duas rotas de leitura que você chama no boot do conector. `GET /v1/ping` prova que a credencial, o relógio e o ambiente estão certos. `GET /v1/currencies` diz quais moedas e métodos a sua operação aceita e o que o `POST /v1/deposits` exige dela. ### O que é | Rota | Escopo | `operationId` | O que devolve | |---|---|---|---| | `GET /v1/ping` | qualquer chave válida | `ping` | operação e ambiente derivados da chave, mais o relógio do servidor | | `GET /v1/currencies` | `read` | `listarMoedas` | moedas habilitadas na sua operação, com os métodos e as exigências de cada um | Nenhuma das duas cria nem altera nada. O ambiente e a operação vêm **da chave**, nunca de um campo do pedido. ### Quando usar - **No boot do conector e em cada deploy:** chame `GET /v1/ping`. Se o pedido volta `200`, a chave existe, a assinatura está certa e o relógio está dentro da janela. - **Para detectar desvio de relógio:** compare `server_time` com o seu relógio. Alerte quando a diferença passar de 5 s. A janela de aceitação da assinatura é de ±300 s, mas desvio perto disso causa `401` intermitente. - **Para confirmar o ambiente:** `environment` é `sandbox` numa operação Teste e `production` numa operação Real. Se vier o contrário do esperado, você está com a chave ou o host errado (veja [Ambientes, tipos de operação e hosts](../conceitos/ambientes-e-hosts.md)). - **Antes de oferecer um método ao jogador:** leia `GET /v1/currencies`. Só ofereça o que a lista traz. - **Antes de montar o corpo do `POST /v1/deposits`:** leia `method_details` para saber que campos do jogador a operação exige. ### Como funciona #### `GET /v1/ping` Resposta `200`: | Campo | Tipo | Conteúdo | |---|---|---| | `ok` | booleano | sempre `true` | | `operation_id` | texto | UUID da operação a que a chave pertence | | `environment` | texto | `sandbox` (operação Teste) ou `production` (operação Real) | | `server_time` | texto | instante do servidor, RFC 3339 em UTC com segundos | O ping **não** é afetado pela queda simulada de `POST /v1/sandbox/outage`: ele continua `200` mesmo com a queda ligada. Qualquer chave válida pode chamá-lo, sem exigência de escopo. #### `GET /v1/currencies` Resposta `200` com `{"data": [Moeda, ...]}`. Cada `Moeda` tem: | Campo | Tipo | Significado | |---|---|---| | `code` | texto | código da moeda. Hoje só `BRL` | | `kind` | texto | `fiat` ou `crypto`. Hoje só `fiat` | | `exponent` | inteiro | casas da menor unidade. `BRL` tem `2`: R$ 50,00 é `"5000"` em `Money.amount` | | `methods` | lista de texto | métodos habilitados para a moeda **nesta operação**. Hoje só `pix` | | `method_details` | lista de objetos | um item por método de `methods`, na mesma ordem. Veja abaixo | | `rails` | — | **nunca vem**. Está no contrato 1.1.0 para trilhos bancários, que não existem hoje | A lista é a interseção de duas coisas: o que o Dracma suporta e o que a sua operação habilitou. Moeda ou método fora do que o Dracma suporta nunca aparece, mesmo que a operação o tenha configurado. Operação sem moeda habilitada recebe `{"data": []}`. Use `exponent` para converter valores: nunca fixe "2 casas" no código. #### `method_details` e as exigências por operação Cada item de `method_details` tem: | Campo | Tipo | Significado | |---|---|---| | `method` | texto | o método a que o item se refere | | `deposit_requirements` | lista de texto | campos do `Jogador` que o `POST /v1/deposits` desta operação **exige** | | `psp_notes` | lista de texto | avisos informativos sobre o PSP de depósito da operação | - `deposit_requirements` é um subconjunto de `player.name`, `player.email`, `player.ip`, `player.device_id`, sempre nessa ordem. Método sem exigência vem com a lista vazia, nunca `null`. - `psp_notes` hoje pode conter `payer_identity_unavailable`: o PSP de depósito não informa o documento do pagador real. Espere mais depósitos em `under_review` com o motivo `PAYER_UNKNOWN`. Nota desconhecida é informativa: ignore. - O Dracma escolhe o PSP de cada operação no onboarding. Você não escolhe e não precisa saber qual é: leia as exigências aqui. Veja a matriz de PSPs em [PSPs](../dinheiro/psps-e-ambientes.md). `method_details` é um acréscimo do Dracma ao contrato 1.1.0 (campo aditivo). Está no documento OpenAPI servido (schema `DetalheMetodo`) e não no contrato do briefing. Veja [Divergências conhecidas](divergencias.md#exigencias-por-operacao). Leia as exigências no boot e a cada hora. Não as fixe no código: elas mudam com a configuração da operação. !!! note "Reservado no contrato" O campo `rails` de `Moeda` e o schema `TrilhoDisponivel` existem no contrato 1.1.0 para trilhos bancários (wire e SWIFT). O Dracma não habilita nenhum trilho hoje, então `rails` nunca é devolvido. Veja [Rotas e campos reservados](superficies-reservadas.md). ### Exemplos Ping, resposta `200` de uma operação Teste: ```json { "ok": true, "operation_id": "0192a3b4-5c6d-7e8f-9a0b-1c2d3e4f5a6b", "environment": "sandbox", "server_time": "2026-10-18T15:04:05Z" } ``` Um item de `data` em `GET /v1/currencies`, para uma operação cujo PSP de depósito exige nome e e-mail do jogador: ```json { "code": "BRL", "kind": "fiat", "exponent": 2, "methods": ["pix"], "method_details": [ { "method": "pix", "deposit_requirements": ["player.name", "player.email"], "psp_notes": ["payer_identity_unavailable"] } ] } ``` Para uma operação que não exige nada além do básico, `deposit_requirements` e `psp_notes` vêm como `[]`. Pedido assinado, no formato dos demais exemplos da referência (valores de assinatura ilustrativos): ```http GET /v1/currencies HTTP/1.1 Host: api.example.test Dracma-Key-Id: key_test_01J9EXEMPLO Dracma-Timestamp: 1790000000 Dracma-Nonce: 0123456789abcdef0123456789abcdef Dracma-Signature: v1= ``` ### Erros e casos de borda | Situação | Resposta | |---|---| | Assinatura, relógio, nonce ou chave inválidos | `401` com `invalid_signature`, `timestamp_out_of_window`, `replayed_nonce` ou `unknown_key` | | Chave sem o escopo `read` em `GET /v1/currencies` | `403 insufficient_scope` | | Chave `key_live_` num host de Teste, ou o contrário | `401 unknown_key`. É assim que você descobre o ambiente errado | | Banco ou dependência da API fora | `503 service_unavailable`, com `Retry-After`. Repita | | Muitas chamadas | `429 rate_limited`, com `Retry-After` | - `GET /v1/currencies` nunca devolve uma resposta sem as exigências de `method_details`: se a API não consegue lê-las, responde `503` e registra o problema, em vez de entregar uma lista que enganaria o conector. - Um método ausente de `methods` responde `400 method_not_enabled` no `POST /v1/deposits`; uma moeda ausente responde `400 currency_not_enabled`. - Valor desconhecido em `kind`, `methods` ou `psp_notes` é informativo: aplique o leitor tolerante (veja [Convenções HTTP](../conceitos/convencoes-http.md)). ### Veja também - [Depósitos](depositos.md): o que acontece quando uma exigência não é cumprida. - [Autenticação HMAC](../api/autenticacao.md) - [Ambientes, tipos de operação e hosts](../conceitos/ambientes-e-hosts.md) - [Objetos e campos](schemas.md) --- # Arquivo: docs/referencia/depositos.md ## Depósitos > Para: Dev — Conferido em 2026-10-18. Um depósito é uma intenção de cobrança PIX: você o cria, o Dracma devolve o copia-e-cola, o jogador paga e o Dracma avisa por webhook. Esta página descreve as quatro rotas (criar, consultar, listar e cancelar), a validação do corpo, os erros na ordem em que a API os confere e os campos que o objeto `Deposito` realmente devolve. ### O que é | Rota | Escopo | `operationId` | Resposta de sucesso | |---|---|---|---| | `POST /v1/deposits` | `deposits:write` | `criarDeposito` | `201` com o PIX pronto, ou `202` com o depósito ainda em `created` | | `GET /v1/deposits` | `read` | `listarDepositos` | `200` com `{data, next_cursor}` | | `GET /v1/deposits/{id}` | `read` | `obterDeposito` | `200` com o `Deposito` | | `POST /v1/deposits/{id}/cancel` | `deposits:write` | `cancelarDeposito` | `200` com o `Deposito` em `cancelled` | Hoje só existe **PIX em BRL**. Qualquer outro método do enum `Metodo` responde `400 method_not_enabled`. | `method` | Situação | |---|---| | `pix` | disponível | | `card`, `crypto`, `boleto`, `bank_transfer`, `wire`, `swift` | **não disponível**: `400 method_not_enabled` | | qualquer outro valor (por exemplo `"ted"`) | `400 validation_failed` com `errors[0] = {field: "method", reason: "enum"}` | !!! warning "Não disponível" O Dracma não oferece hoje cartão, cripto, boleto, transferência bancária, wire nem SWIFT para depósito. A tentativa responde `400 method_not_enabled` e não cria nada. O único método é `pix`, na moeda `BRL`. O Dracma credita o **valor cheio** no depósito pago. O objeto `Deposito` não traz tarifa descontada: o campo `fee` do contrato não é devolvido (veja a seção "Objeto `Deposito`" mais abaixo). As tarifas do contrato com o Dracma estão em [Tarifas](../dinheiro/tarifas.md). ### Quando usar O fluxo recomendado, de ponta a ponta: ```mermaid sequenceDiagram participant C as Seu conector participant D as API do Dracma participant W as Webhook do Dracma C->>C: grava o depósito local (external_id, amount, player_ref) C->>D: POST /v1/deposits (Idempotency-Key = external_id) D-->>C: 201 pending com instructions.pix.copy_paste C->>C: mostra o copia-e-cola até instructions.pix.expires_at Note over C,W: o jogador paga no banco W->>C: POST deposit.paid (assinado) C->>C: verifica assinatura e deduplica C->>D: GET /v1/deposits/{id} (fetch-back) D-->>C: 200 status paid C->>C: credita uma vez, pelo valor que você gravou ``` 1. Grave o depósito no seu lado **antes** de chamar o Dracma. 2. Chame `POST /v1/deposits` com `Idempotency-Key` igual ao `external_id`. Use timeout de cliente de 10 s. 3. Em erro de rede, `5xx` ou `429`, repita com a **mesma** chave. Se perdeu a resposta, consulte `GET /v1/deposits?external_id=`. 4. Mostre `instructions.pix.copy_paste` ao jogador até `instructions.pix.expires_at`. 5. Ao receber `deposit.paid`, verifique a assinatura, deduplique e faça o **fetch-back** com `GET /v1/deposits/{id}`. Só credite quando o `status` lido for `paid`. Nunca credite pelo corpo do webhook. O efeito de cada estado no seu livro está em [Ciclo do depósito](../dinheiro/ciclo-do-deposito.md). ### Como funciona #### Criar: `POST /v1/deposits` O corpo é um objeto JSON com estas chaves. Qualquer chave fora da lista é recusada. | Campo | Obrigatório | Regra | |---|---|---| | `external_id` | sim | seu id do depósito, `^[A-Za-z0-9_.:-]{1,64}$`, único por operação | | `player.external_ref` | sim | seu id do jogador, mesmo formato, opaco e imutável (nunca e-mail, telefone ou CPF) | | `player.document` | sim | `type` em `CPF`, `CNPJ`, `PASSPORT`, `NATIONAL_ID`, `TAX_ID`; CPF/CNPJ só dígitos (11 a 14); `country` ISO 3166 alfa-2 em maiúsculas | | `player.name` | depende da operação | até 120 caracteres | | `player.email` | depende da operação | validado sempre que vier. Veja "Exigências por operação" | | `player.ip`, `player.device_id` | depende da operação | informativos, entram na análise de risco. Veja "Exigências por operação" | | `player.birth_date`, `player.kyc_level` | não | informativos | | `amount` | sim | `Money` com `amount` maior que zero | | `method` | sim | `pix` | | `expires_in_seconds` | não | de 300 a 86400. Padrão 1800 | | `locale` | não | por exemplo `pt-BR` | | `metadata` | não | até 20 chaves, valores texto | Para PIX o documento do jogador só pode ser CPF ou CNPJ. Os outros tipos passam na validação de forma, mas voltam `422 document_invalid`. O documento do jogador fica **fixado** no primeiro uso do `external_ref`. Mandar outro documento para o mesmo `external_ref` depois resulta em `422 document_mismatch`. Veja [Dados e privacidade](../seguranca/dados-e-privacidade.md). **Validação estrita.** O corpo é recusado com `400 validation_failed`, antes de qualquer efeito, quando tem: | Problema | `errors[].reason` | |---|---| | campo desconhecido, inclusive `status`, `version`, `paid_amount` ou `operation_id` aninhado | `unknown_field` | | chave JSON repetida em qualquer nível | `duplicate_key` | | UTF-8 inválido | `invalid_utf8` | | JSON inválido | `syntax` | | conteúdo depois do objeto | `trailing_data` | | tipo errado | `type` | | `external_id`, `external_ref`, número de documento ou país em formato inválido | `format` | | tipo de documento fora da lista | `enum` | | `expires_in_seconds` fora da faixa, ou `metadata` com mais de 20 chaves | `range` | | `amount` ausente ou zero, `method` ausente | `required` | `operation_id` na **raiz** do corpo é descartado em silêncio: o tenant e a operação vêm só da credencial. Não o envie. `method_options` e `fx_quote_id` do documento OpenAPI **não** são aceitos em depósito PIX: voltam `unknown_field` (veja [Divergências conhecidas](divergencias.md#depositos)). Corpo acima de 64 KiB volta `413`. #### Exigências por operação Alguns PSPs exigem dados que o contrato deixa opcionais. O Dracma escolhe o PSP de depósito de cada operação no onboarding e declara as exigências em `GET /v1/currencies` (`method_details[].deposit_requirements`). Veja [Ping e moedas](ping-e-moedas.md#method_details-e-as-exigencias-por-operacao). Quando a operação exige um campo e ele vem ausente, em branco ou só com espaços, a API recusa **antes** de criar qualquer coisa e antes do motor de regras: | `errors[].field` | `errors[].reason` | Quando | |---|---|---| | o campo exigido, por exemplo `player.email` | `required_by_operation` | a operação exige e o campo não veio | | `player.email` | `format` | fora de `local@dominio.tld`, ou com nome de exibição. Vale sempre que o e-mail vier | | `player.email` | `max_length` | mais de 254 caracteres | | `player.email` | `reserved_domain_required` | **operação Teste**: o e-mail só pode ter TLD reservado (`.test`, `.invalid`, `.example`) | | `player.name` | `max_length` | mais de 120 caracteres. Vale sempre | - É um `4xx`: não cria objeto, jogador nem evento. A mesma `Idempotency-Key` devolve o mesmo `400` por 30 dias. Depois de corrigir o cadastro, crie com **`external_id` novo**. - Mande o e-mail **cadastrado** do jogador, lido do seu servidor. Nunca um e-mail digitado no pedido de depósito e nunca um inventado. - Em operação Teste, e-mail real de um cadastro copiado de produção recebe `reserved_domain_required`. Reescreva os e-mails do seu ambiente de teste (por exemplo `jogador1@operador.test`). Se a operação não exige e-mail, não envie o campo. - Mande sempre `player.name`: operações reais costumam exigi-lo. - Em operação Real o domínio do e-mail não é restrito, só o formato. #### Resposta `201`: PIX pronto O depósito é criado e o PSP já devolveu o PIX. `status` vem `pending`, `version` vem `2` e `instructions.pix` traz `copy_paste` (o BR Code, que é o conteúdo do QR) e `expires_at`. `qr_code_png_base64` vem quando o PSP o fornece; se não vier, gere o QR a partir do `copy_paste`. - Use o `instructions.pix.expires_at` **devolvido**, não o prazo que você pediu: o PSP pode ajustar o prazo. - Em operação Teste o BR Code é sintético e nenhum dinheiro real se move. - `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. !!! danger "Dinheiro real" Em uma operação **Real**, o PIX devolvido é um PIX de verdade: o pagamento move dinheiro real. Faça seus testes de ponta a ponta numa operação Teste. #### Resposta `202`: aceito, PIX ainda não disponível Se o PSP demorar mais que o orçamento de resposta da API (cerca de 7 s), o Dracma responde `202` com `status: "created"`, `version: 1` e `instructions: null`. Não é erro. Espere o webhook `deposit.pending` ou consulte `GET /v1/deposits/{id}` até o `status` ser `pending`, e então mostre o PIX. **Não crie outro depósito** para tentar de novo. #### Consultar: `GET /v1/deposits/{id}` É o **fetch-back**: o estado autoritativo que você lê antes de creditar. `200` devolve o `Deposito`. Um `id` que não existe **nesta operação** responde `404 not_found`. Objeto de outra operação também é `404`, nunca `403`, e um `id` fora do formato `dep_` seguido de letras e números também. Antes de creditar, confira o que veio contra o que **você** gravou na criação: `external_id`, `amount.amount`, `amount.currency` e `player_ref` têm de ser idênticos. Se algo divergir, não credite, marque para revisão e alerte. #### Listar: `GET /v1/deposits` Escopo `read`. Usa o balde de listagem do rate limit (veja [Limites e quotas](../api/limites.md#baldes-de-taxa)). | Parâmetro | Uso | |---|---| | `external_id` | acha o depósito pelo **seu** id. É o jeito de saber o destino de um `POST` cuja resposta se perdeu | | `player_ref` | depósitos de um jogador | | `status` | filtra por estado. Valor fora do enum de `StatusDeposito` responde `400` com `reason: format` | | `updated_since` | RFC 3339. Devolve itens com `updated_at` **maior ou igual** ao instante dado | | `cursor` | o `next_cursor` da página anterior. Cursor inválido responde `400` com `reason: format` | | `limit` | de 1 a 200. Padrão 50. Valor fora da faixa responde `400` com `reason: format` | A resposta é `{"data": [Deposito, ...], "next_cursor": "dep_..." | null}`. A ordem é a do **id do depósito**, que segue a ordem de criação, e **não** a de `updated_at`. `next_cursor` é o id `dep_...` do último item da página e vira `null` na última. Para uma varredura incremental, guarde o maior `updated_at` visto e use `updated_since` com ele (o `>=` repete o último item, então deduplique). Veja [Paginação e filtros](../api/paginacao.md#cursor-ordem-e-filtros-por-endpoint). Assine o caminho **com a query exatamente como enviada**. #### Cancelar: `POST /v1/deposits/{id}/cancel` Escopo `deposits:write`. Corpo vazio. `Idempotency-Key` obrigatória. - Só cancela de `pending`. A resposta é `200` com o objeto em `cancelled`. Se ele **já estava** `cancelled`, a resposta é o mesmo `200`, sem evento novo. - Em `created`, `paid`, `expired`, `under_review` ou qualquer outro estado, a resposta é `409 invalid_state_transition`. Em `created`, espere `pending` e cancele depois. - **Cancelar no Dracma não cancela a cobrança no PSP.** O PIX continua pagável até o prazo do PSP. Se alguém pagar depois, o estado **não muda** e o `paid_amount` é gravado: não credite e trate como exceção. Veja [Política de exceções de dinheiro](../conector/politica-de-excecoes.md) e a divergência D-D2 em [Divergências conhecidas](divergencias.md#depositos). #### Expiração O Dracma expira sozinho o depósito `pending` depois de `expires_at` mais 2 minutos 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`. ### Objeto `Deposito` O `Deposito` é o mesmo no `201`, no `GET` e no `data.object` do webhook. A API devolve **16 campos** e nenhum outro. Os campos da v1.1.0 do contrato (`rail`, `received_amount`, `receipts`, `fee`, `fees`, `fx`, `compliance`, `expected_settlement_date`, `return`) existem no documento OpenAPI, mas **nunca** são devolvidos em depósito. Não dependa deles. | Campo | Tipo | Devolvido hoje? | Conteúdo | |---|---|---|---| | `id` | texto | sim | id do Dracma, `dep_` seguido de 32 caracteres hexadecimais. Trate como opaco | | `external_id` | texto | sim | o seu id | | `status` | texto | sim | estado atual | | `version` | inteiro | sim | começa em `1` (`created`) e sobe a cada transição. Ignore estado com `version` menor ou igual à já aplicada | | `amount` | `Money` | sim | valor cobrado | | `paid_amount` | `Money` ou `null` | sim | valor pago. Em `paid` é igual a `amount`. Pode vir preenchido em estado terminal sem crédito (pagamento tardio) | | `method` | texto | sim | `pix` | | `player_ref` | texto | sim | o seu `player.external_ref` | | `payer` | objeto ou `null` | sim | quem pagou. Preenchido em `paid` e `under_review`. Veja abaixo | | `instructions` | objeto ou `null` | sim | `null` em `created`. Depois, `{"pix": {copy_paste, expires_at, qr_code_png_base64?}}` | | `failure_code` | texto ou `null` | sim | motivo técnico em `failed`. Lista aberta | | `review_reasons` | lista de texto | sim | motivos de `under_review`. Vazia (`[]`) fora de revisão | | `created_at`, `updated_at` | texto | sim | RFC 3339 em UTC **com segundos, sem fração**, por exemplo `2026-10-18T15:04:05Z` | | `paid_at` | texto ou `null` | sim | instante do pagamento | | `expires_at` | texto ou `null` | sim | fim do prazo do PIX | | `rail`, `received_amount`, `receipts`, `fee`, `fees`, `fx`, `compliance`, `expected_settlement_date`, `return` | — | **não** | reservados da v1.1.0 | !!! note "Reservado no contrato" `fee`, `fees`, `fx`, `compliance`, `return` e os demais campos da v1.1.0 estão no schema `Deposito` do OpenAPI, mas a API não os preenche em depósito PIX. A tarifa não é descontada do valor creditado (veja [Tarifas](../dinheiro/tarifas.md)). **`payer`.** Traz `document` (`type`, `number`, `country`), `name` e `matches_player`, que é `true` quando o documento de quem pagou bate com o do jogador. Use-o só para exibir e auditar; a decisão de crédito é o `status`. **`status`.** O enum `StatusDeposito` tem 13 valores: | `status` | Como o depósito chega aqui | Alcançável em PIX hoje | |---|---|---| | `created` | aceito, PIX ainda não gerado | sim | | `pending` | PIX gerado, aguardando pagamento | sim | | `under_review` | dinheiro recebido com divergência (pagador diferente, valor diferente, pago após o prazo ou pagador não confirmado) | sim | | `paid` | pago com valor exato e pagador conferido, ou aprovado na revisão | sim | | `expired` | prazo vencido sem pagamento | sim | | `cancelled` | cancelado por você | sim | | `failed` | o PSP recusou a cobrança | sim | | `returned` | devolvido na revisão: o depósito não foi aceito e nunca foi creditado | sim | | `refunded`, `chargeback` | depois de `paid` | só no sandbox, por ação simulada | | `pending_compliance`, `frozen`, `paid_partial` | acréscimos da v1.1.0 | **não**: nenhum depósito chega a esses estados hoje | Estado desconhecido é **não-terminal e não move dinheiro** no seu lado: confira de novo pelo fetch-back (leitor tolerante). A máquina de estados completa e o efeito de cada estado no seu livro estão em [Ciclo do depósito](../dinheiro/ciclo-do-deposito.md). **`review_reasons`.** Lista aberta de códigos em maiúsculas, por exemplo `PAYER_MISMATCH`, `PAYER_UNKNOWN`, `AMOUNT_MISMATCH`, `PAID_AFTER_EXPIRY`, `RULES_UNAVAILABLE` e `RULES_INVALID_RESPONSE`. Código desconhecido não muda nada: o que manda é o `status`. O significado de cada um e o que o painel faz com ele está em [Ciclo do depósito](../dinheiro/ciclo-do-deposito.md). ### Exemplos Pedido de criação (os valores de assinatura são ilustrativos; o algoritmo está em [Autenticação HMAC](../api/autenticacao.md#pedido-canonico-e-assinatura)): ```http POST /v1/deposits HTTP/1.1 Host: api.example.test 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= { "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 } ``` Resposta `201`, PIX pronto: ```json { "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...6304ABCD", "expires_at": "2026-10-18T15:30:00Z" } }, "failure_code": null, "review_reasons": [], "created_at": "2026-10-18T15:00:00Z", "updated_at": "2026-10-18T15:00:01Z", "paid_at": null, "expires_at": "2026-10-18T15:30:00Z" } ``` Resposta `202`, PIX ainda não disponível: ```json { "id": "dep_0192a3b40c6d7e8f9a0b1c2d3e4f5a6c", "external_id": "dep_0192a3b4-5c6d-7e8f-9a0b-1c2d3e4f5a6c", "status": "created", "version": 1, "amount": { "amount": "5000", "currency": "BRL" }, "paid_amount": null, "method": "pix", "player_ref": "0192a3b4-0000-7000-8000-000000000001", "payer": null, "instructions": null, "failure_code": null, "review_reasons": [], "created_at": "2026-10-18T15:00:00Z", "updated_at": "2026-10-18T15:00:00Z", "paid_at": null, "expires_at": "2026-10-18T15:30:00Z" } ``` O mesmo depósito depois de pago, lido no fetch-back: ```json { "id": "dep_0192a3b40c6d7e8f9a0b1c2d3e4f5a6b", "external_id": "dep_0192a3b4-5c6d-7e8f-9a0b-1c2d3e4f5a6b", "status": "paid", "version": 3, "amount": { "amount": "5000", "currency": "BRL" }, "paid_amount": { "amount": "5000", "currency": "BRL" }, "method": "pix", "player_ref": "0192a3b4-0000-7000-8000-000000000001", "payer": { "document": { "type": "CPF", "number": "12345678909", "country": "BR" }, "name": "Jogador Teste", "matches_player": true }, "instructions": { "pix": { "copy_paste": "00020126...6304ABCD", "expires_at": "2026-10-18T15:30:00Z" } }, "failure_code": null, "review_reasons": [], "created_at": "2026-10-18T15:00:00Z", "updated_at": "2026-10-18T15:07:41Z", "paid_at": "2026-10-18T15:07:40Z", "expires_at": "2026-10-18T15:30:00Z" } ``` Um depósito em revisão porque o pagador não é o jogador. **Não credite**: ```json { "id": "dep_0192a3b40c6d7e8f9a0b1c2d3e4f5a6d", "external_id": "dep_0192a3b4-5c6d-7e8f-9a0b-1c2d3e4f5a6d", "status": "under_review", "version": 3, "amount": { "amount": "5000", "currency": "BRL" }, "paid_amount": { "amount": "5000", "currency": "BRL" }, "method": "pix", "player_ref": "0192a3b4-0000-7000-8000-000000000002", "payer": { "document": { "type": "CPF", "number": "98765432100", "country": "BR" }, "name": "Outra Pessoa", "matches_player": false }, "instructions": { "pix": { "copy_paste": "00020126...6304ABCD", "expires_at": "2026-10-18T15:30:00Z" } }, "failure_code": null, "review_reasons": ["PAYER_MISMATCH"], "created_at": "2026-10-18T15:00:00Z", "updated_at": "2026-10-18T15:09:10Z", "paid_at": "2026-10-18T15:09:09Z", "expires_at": "2026-10-18T15:30:00Z" } ``` Recusa por exigência da operação (`400`, sem objeto criado): ```json { "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" } ] } ``` Listagem para achar um depósito pelo seu id: ```http GET /v1/deposits?external_id=dep_0192a3b4-5c6d-7e8f-9a0b-1c2d3e4f5a6b&limit=10 HTTP/1.1 ``` ### 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 (veja a tabela de validação) | corrija o pedido; não repita igual | | 2 | 429 | `rate_limited` | mais de 10 depósitos criados por jogador por hora | mostre "tente mais tarde"; repita com a mesma chave depois do `Retry-After` | | 3 | 400 | `validation_failed` (`field: method`, `reason: enum`) | `method` fora do enum `Metodo` | corrija | | 4 | 400 | `method_not_enabled` | `method` do enum, mas diferente de `pix` | não ofereça o método | | 5 | 422 | `document_invalid` | tipo de documento diferente de CPF e CNPJ | revise o cadastro do jogador | | 6 | 409 | `duplicate_external_id` | o `external_id` já existe (com outra `Idempotency-Key`); o objeto vem em `existing` | use o `existing`; não crie outro | | 7 | 400 | `currency_not_enabled` ou `method_not_enabled` | moeda ou método não habilitado **nesta operação** | configuração do onboarding | | 8 | 400 | `validation_failed` (`required_by_operation`, `format`, `max_length`, `reserved_domain_required`) | exigência da operação | corrija o cadastro; `external_id` novo | | 9 | 422 | `amount_below_minimum`, `amount_above_maximum`, `document_invalid`, `document_mismatch`, `player_blocked`, `destination_blocked`, `velocity_exceeded` | recusa do motor de regras, sem objeto | mostre mensagem genérica ao jogador; não revele a causa | | 10 | 503 | `service_unavailable` | motor de regras, cofre de dados ou banco fora | repita com a **mesma** `Idempotency-Key` depois do `Retry-After` | Notas: - `document_invalid` no passo 9 cobre CPF ou CNPJ com dígito verificador errado. `velocity_exceeded` no depósito cobre muitos depósitos criados e não pagos numa janela. Os limiares são da regra da operação: consulte [Regras (API)](regras.md) e [Decisão, limites e regras](../dinheiro/decisao-limites-e-regras.md). - **Recusa `4xx` não cria objeto.** A mesma `Idempotency-Key` devolve a mesma recusa, guardada por 30 dias. Se você usa `Idempotency-Key = external_id`, tentar de novo depois de uma recusa exige **`external_id` novo**. Veja [Idempotência e repetição](../api/idempotencia.md#convencao-recomendada-chave-igual-ao-external_id). - Erro de rede, timeout, `5xx` e `429` não são respostas conclusivas: repita com a mesma chave. #### Erros das outras rotas | Rota | Situação | Resposta | |---|---|---| | `GET /v1/deposits/{id}` | não existe nesta operação, ou `id` fora do formato | `404 not_found` | | `GET /v1/deposits` | `status`, `updated_since`, `limit` ou `cursor` inválidos | `400 validation_failed` com o campo em `errors[]` | | `POST /v1/deposits/{id}/cancel` | estado diferente de `pending` e de `cancelled` | `409 invalid_state_transition` | | `POST /v1/deposits/{id}/cancel` | não existe nesta operação | `404 not_found` | | qualquer | sem o escopo exigido | `403 insufficient_scope` | #### Casos de borda - **Pagamento depois de expirar ou cancelar.** Se o PIX é pago com o depósito já em `expired`, `cancelled` ou `failed`, o estado **não muda**: o Dracma grava `paid_amount`, `paid_at` e o pagador e alerta. Para você, `paid_amount` preenchido num estado sem crédito é exceção: não credite. Veja [Ciclo do depósito](../dinheiro/ciclo-do-deposito.md#pagamento-depois-do-prazo-ou-do-cancelamento). - **`under_review` não credita.** O dinheiro entrou, mas há divergência. Espere o desfecho (`paid` ou `returned`). - **Estado e evento novos.** Aplique o leitor tolerante: campo, estado ou motivo desconhecido nunca devem derrubar o seu parser nem mover dinheiro. - **Ambiente.** Em operação Teste, e-mail com domínio real é recusado (`reserved_domain_required`) e nenhum dinheiro se move. ### Veja também - [Ciclo do depósito](../dinheiro/ciclo-do-deposito.md): estados, efeito no seu livro e pagamento tardio. - [Ping e moedas](ping-e-moedas.md): `deposit_requirements`. - [Erros](../api/erros.md) e [Idempotência e repetição](../api/idempotencia.md) - [Webhooks](../webhooks/README.md) e [Catálogo de eventos](../webhooks/catalogo-de-eventos.md) - [Sandbox](../sandbox/README.md): como simular pagamento, expiração, estorno e chargeback. - [Objetos e campos](schemas.md) --- # Arquivo: docs/referencia/saques.md ## 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](superficies-reservadas.md). 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` | !!! warning "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](../dinheiro/psps-e-ambientes.md). ### 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=`. ```mermaid 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](../dinheiro/ciclo-do-saque.md). ### 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](depositos.md) 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](divergencias.md#saques)). #### 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](../dinheiro/decisao-limites-e-regras.md). Os limiares são da regra da operação: leia com [Regras (API)](regras.md). #### 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](../api/limites.md#baldes-de-taxa)). | 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](../api/paginacao.md#cursor-ordem-e-filtros-por-endpoint). #### 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](divergencias.md#saques)). - `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). !!! note "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](../dinheiro/tarifas.md)). #### `status` e transições ```mermaid 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](../dinheiro/ciclo-do-saque.md#banco-devolveu-o-pix) | | `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](reconciliacao.md#os-itens)). - `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](../conector/politica-de-excecoes.md)). ### Exemplos Pedido de criação (os valores de assinatura são ilustrativos): ```http 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= { "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: ```json { "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): ```json { "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): ```json { "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): ```json { "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: ```http GET /v1/withdrawals?external_id=wdr_0192a3b4-5c6d-7e8f-9a0b-1c2d3e4f5a6c HTTP/1.1 ``` !!! danger "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](../sandbox/README.md). ### 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](../api/idempotencia.md). #### 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 - [Ciclo do saque](../dinheiro/ciclo-do-saque.md): reserva, `processing`, `approved` parado e `returned`. - [Decisão, limites e regras](../dinheiro/decisao-limites-e-regras.md): `decision.reasons`. - [Limites do jogador e saldos](jogadores-e-saldos.md): quanto cabe no automático e o caixa da operação. - [Rotas e campos reservados](superficies-reservadas.md): recall e campos da v1.1.0. - [Erros](../api/erros.md), [Idempotência e repetição](../api/idempotencia.md) e [Webhooks](../webhooks/README.md) - [Sandbox](../sandbox/README.md) --- # Arquivo: docs/referencia/jogadores-e-saldos.md ## Limites do jogador e saldos > Para: Dev, Fin — Conferido em 2026-10-18. Duas leituras: `GET /v1/players/{external_ref}/limits` mostra a posição de um jogador perante as regras de saque automático, e `GET /v1/balances` mostra o caixa de liquidação da sua operação por moeda. Esta página descreve a **forma** das respostas. O significado financeiro (o que entra no caixa, o que é caixa livre, como o multiplicador age) está em [Saldos e caixa livre](../dinheiro/saldos.md) e em [Decisão, limites e regras](../dinheiro/decisao-limites-e-regras.md). ### O que é | Rota | Escopo | `operationId` | O que devolve | |---|---|---|---| | `GET /v1/players/{external_ref}/limits?currency=` | `read` | `limitesDoJogador` | `LimitesJogador`: totais do jogador, multiplicador e o que ainda cabe no automático | | `GET /v1/balances` | `read` | `saldosDaOperacao` | `{"data": [...]}` com um item por moeda habilitada | As duas são somente leitura e não têm efeito colateral. ### Quando usar - **Avisar o jogador antes do pedido.** Use `auto_withdrawal_remaining` para dizer "até R$ X sai na hora". É uma estimativa: outras regras (primeiro saque, chave nova, janelas, saque sem `player.ip`) ainda podem mandar o saque para revisão. - **Decidir se oferece o saque.** `blocked: true` indica que o jogador está numa lista de bloqueio. - **Monitorar o caixa.** Alerte quando o caixa livre se aproximar do valor dos saques que você espera. Um saque sem caixa recebe `422 insufficient_operation_balance` na criação, ou fica em `approved` se o caixa faltar só no envio. - **Reconciliar.** Os saldos servem para acompanhamento. A prova diária está em [Reconciliação](reconciliacao.md). ### Como funciona #### `GET /v1/players/{external_ref}/limits` O caminho leva o `external_ref` do jogador (`^[A-Za-z0-9_.:-]{1,64}$`) e a query leva `currency`, que é **obrigatório**. A API confere nesta ordem: 1. `external_ref` fora do formato: `400 validation_failed` com `field: external_ref` e `reason: format`. 2. `currency` ausente: `400` com `field: currency` e `reason: required`. 3. Moeda não habilitada na operação: `400 currency_not_enabled`. 4. Jogador que nunca transacionou **nesta operação**: `404 not_found`. Jogador de outra operação também é `404`: a API nunca confirma a existência. 5. Motor de regras fora: `503 service_unavailable`, repetível. Resposta `200`, objeto `LimitesJogador`: | Campo | Tipo | Conteúdo | |---|---|---| | `player_ref` | texto | o `external_ref` pedido | | `currency` | texto | a moeda pedida | | `total_deposited` | `Money` | total depositado pelo jogador e liquidado pelo Dracma, na moeda, conforme a regra da operação | | `total_withdrawn` | `Money` | total de saques do jogador, contado pela regra da operação | | `auto_multiplier` | texto | o multiplicador X da regra, decimal em texto (`"1"`, `"1.5"`). `"0"` desliga o automático | | `auto_withdrawal_remaining` | `Money` | quanto ainda cabe no saque automático pela regra do multiplicador | | `blocked` | booleano | `true` quando o jogador ou o documento dele está em lista de bloqueio | As janelas internas de consumo e a versão do conjunto de regras que o motor calcula **não** são devolvidas. #### `GET /v1/balances` Resposta `200` com `{"data": [Saldo, ...]}`, uma entrada por moeda habilitada, ordenadas pelo código da moeda. Cada entrada tem: | Campo | Tipo | Conteúdo | |---|---|---| | `available` | `Money` | o caixa da operação no livro do Dracma. **Inclui** o que já está reservado para saques em curso | | `pending_in` | `Money` | dinheiro já recebido e ainda não creditado: depósitos em revisão (e nos estados de retenção, que não ocorrem em PIX hoje). Vem do valor pago, ou do pedido se não houver | | `pending_out` | `Money` | o que está reservado para saques em curso, e que continua dentro de `available` | | `as_of` | texto | instante da leitura, RFC 3339 em UTC | - A moeda **não** aparece como campo solto da entrada: ela está dentro de cada `Money`. - Moeda habilitada sem movimento vem zerada (`"0"`). - O **caixa livre** para novos saques é `available` menos `pending_out`. A API não devolve esse número pronto. - `pending_in` é calculado pela API, não vem do livro. Use só para acompanhamento. - Livro fora: `503 service_unavailable`, repetível. !!! warning "Não disponível" O Dracma não oferece aporte de caixa: o caixa da operação só cresce por depósito pago ou por devolução de saque. Não existe rota para recarregar o saldo. Se o caixa faltar, o saque novo recebe `422 insufficient_operation_balance`. ### Exemplos Pedido: ```http GET /v1/players/0192a3b4-0000-7000-8000-000000000001/limits?currency=BRL HTTP/1.1 Host: api.example.test Dracma-Key-Id: key_test_01J9EXEMPLO Dracma-Timestamp: 1790000100 Dracma-Nonce: 0123456789abcdef0123456789abcdef Dracma-Signature: v1= ``` Resposta `200`: ```json { "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 } ``` `GET /v1/balances`, resposta `200`: ```json { "data": [ { "available": { "amount": "150000000", "currency": "BRL" }, "pending_in": { "amount": "250000", "currency": "BRL" }, "pending_out": { "amount": "90000", "currency": "BRL" }, "as_of": "2026-10-18T15:00:00Z" } ] } ``` Neste exemplo o caixa livre é R$ 1.500.000,00 − R$ 900,00, ou `"150000000"` − `"90000"` = `"149910000"` na menor unidade. ### Erros e casos de borda | Rota | Situação | Resposta | |---|---|---| | limites | `external_ref` inválido, `currency` ausente | `400 validation_failed` | | limites | moeda não habilitada | `400 currency_not_enabled` | | limites | jogador desconhecido nesta operação | `404 not_found` | | limites e saldos | motor de regras ou livro fora | `503 service_unavailable` com `Retry-After`; repita | | ambas | sem o escopo `read` | `403 insufficient_scope` | - Jogador novo, que ainda não pediu nada, devolve `404` nos limites. Trate como "sem histórico": mostre o texto padrão ao jogador. - `auto_withdrawal_remaining` é uma estimativa. Não a use para liberar saldo no seu lado: quem decide é o `decision` do saque. - Os totais são da regra da operação, não do seu livro. Eles podem diferir dos seus por pendentes ou por depósitos ainda em revisão. ### Veja também - [Saldos e caixa livre](../dinheiro/saldos.md) - [Decisão, limites e regras](../dinheiro/decisao-limites-e-regras.md) - [Saques](saques.md) - [Saldos e caixa no painel](../painel/saldos-e-caixa.md) --- # Arquivo: docs/referencia/regras.md ## Regras (API) > Para: Dev — Conferido em 2026-10-18. A API deixa você **ler** as regras vigentes da sua operação e **propor** uma mudança. Ela nunca altera uma regra: a proposta fica pendente até outra pessoa aprová-la no painel. Esta página descreve `GET /v1/rules`, `POST /v1/rules/change-requests`, o uso de `If-Match` e `ETag` e os erros. ### O que é | Rota | Escopo | `operationId` | Resposta | |---|---|---|---| | `GET /v1/rules` | `read` | `obterRegras` | `200` com o conjunto de regras vigente (`ConjuntoDeRegras`) e o cabeçalho `ETag` | | `POST /v1/rules/change-requests` | `rules:propose` | `pedirMudancaDeRegras` | `202` com `{"id": "chg_...", "status": "pending_approval"}` | O conjunto de regras define os limites de depósito e saque, o multiplicador do saque automático, o horário do automático e outros critérios de revisão. Quem aplica as regras é o motor do Dracma, em cada pedido. Os **códigos** que o motor devolve (`decision.reasons` e os `422`) estão em [Decisão, limites e regras](../dinheiro/decisao-limites-e-regras.md). ### Quando usar - **Ler** quando precisar dos limiares: mínimo e máximo por transação, teto do automático, horário. A página de limites do jogador mostra o que cabe para **um** jogador; `GET /v1/rules` mostra a regra da operação inteira. - **Propor** quando quiser mudar um limite pela API, em vez de pedir ao suporte. Quem tem a chave com o escopo `rules:propose` pode propor; a mudança só vigora depois da aprovação no painel. Os valores de fábrica dos limites não são publicados aqui. Leia os da sua operação com `GET /v1/rules`. ### Como funciona #### Ler: `GET /v1/rules` A API devolve o conjunto vigente **da operação** da chave, no corpo, com o cabeçalho `ETag`. O motor guarda **um** conjunto por operação e a consulta usa a **primeira moeda habilitada** na operação (veja [Ping e moedas](ping-e-moedas.md)). O parâmetro `currency` não existe nesta rota. - O `ETag` é um valor opaco, entre aspas. Guarde-o exatamente como veio, aspas incluídas: você o envia de volta em `If-Match`. - A API não responde `304`: toda leitura devolve o corpo inteiro. - Operação sem moeda habilitada ou sem conjunto de regras: `404 not_found`. - Motor de regras fora: `503 service_unavailable`, repetível. Campos principais do `ConjuntoDeRegras`: | Campo | Tipo | Significado | |---|---|---| | `version` | inteiro | versão do conjunto. Sobe a cada mudança aprovada | | `timezone` | texto | fuso IANA da operação (por exemplo `America/Sao_Paulo`). Define dia, janelas e horário do automático | | `currencies` | objeto | uma chave por moeda; o valor traz as regras daquela moeda (tabela abaixo) | | `first_withdrawal_manual` | booleano | o primeiro saque pago de cada jogador passa por revisão humana | | `pix_key_change_cooldown_hours` | inteiro | carência, em horas, para uma chave PIX nova do jogador | | `require_payer_matches_player` | booleano | exige que o pagador do depósito seja o jogador | | `auto_hours` | objeto ou `null` | `{"from": "HH:MM", "to": "HH:MM"}`, na hora local da operação, ou `null` quando não há restrição de horário | Em `currencies.`: | Campo | Tipo | Significado | |---|---|---| | `auto_multiplier` | texto | multiplicador X do saque automático: o automático cobre até X vezes o que o jogador depositou. `"0"` desliga o automático | | `deposit_min`, `deposit_max` | texto | mínimo e máximo por depósito, na menor unidade | | `withdrawal_min`, `withdrawal_max` | texto | mínimo e máximo por saque, na menor unidade | | `auto_max_per_withdrawal` | texto | teto de um saque no automático | | `auto_max_per_day` | texto | teto do dia no automático | | `dual_approval_above` | texto | acima deste valor, o saque exige duas pessoas na revisão | | `max_withdrawals_per_day` | inteiro | quantidade de saques por dia | O corpo traz **mais grupos de configuração** do que os listados (janelas, velocidade, destino, pagador, calendário do automático e outros). Trate-os como opacos: leia, mantenha e devolva intactos numa proposta. Campo desconhecido é ignorado na leitura (leitor tolerante). !!! note "Reservado no contrato" O grupo `bank_transfers` do conjunto de regras, com `fx` e os limites de wire e SWIFT, é aceito e versionado, mas o motor não o aplica: não há trilho bancário nem câmbio habilitado hoje. Veja [Rotas e campos reservados](superficies-reservadas.md). #### Propor: `POST /v1/rules/change-requests` Uma proposta leva o cabeçalho `If-Match` e um corpo com dois campos: | Campo | Obrigatório | Regra | |---|---|---| | `rules` | sim | um **objeto** com o conjunto de regras proposto | | `justification` | sim | texto não vazio, até 1000 caracteres | Cabeçalhos: `If-Match` (obrigatório), com o `ETag` exato do último `GET /v1/rules`; e `Idempotency-Key` (todo `POST` leva). Fluxo: ```mermaid sequenceDiagram participant C as Seu conector participant D as API do Dracma participant P as Painel C->>D: GET /v1/rules D-->>C: 200 conjunto + ETag C->>C: altera uma cópia do conjunto C->>D: POST /v1/rules/change-requests (If-Match = ETag) alt ETag igual ao vigente D-->>C: 202 {id: chg_..., status: pending_approval} Note over D,P: nada vigora ainda P->>P: uma pessoa da equipe aprova (afrouxar também espera um prazo) P->>D: nova versão vigente else ETag diferente D-->>C: 412 etag_mismatch C->>D: GET /v1/rules e refaça a proposta end ``` 1. Leia `GET /v1/rules` e guarde o corpo e o `ETag`. 2. Altere **uma cópia completa** do conjunto. Não mande só o campo que mudou: blocos ausentes na proposta são preenchidos pelo Dracma com o valor de fábrica, o que pode desfazer configuração que você não quis mexer. 3. Envie `POST /v1/rules/change-requests` com `If-Match` igual ao `ETag` lido. 4. A resposta `202` traz o `id` da proposta (`chg_` seguido de 32 caracteres hexadecimais) e `status: "pending_approval"`. **A regra vigente não mudou.** A API nunca altera uma regra. 5. A aprovação acontece no painel, por uma pessoa da sua equipe com permissão para isso. Mudança que **afrouxa** limites também espera um prazo antes de vigorar. Veja [Regras, listas e emergência](../painel/regras-e-listas.md). 6. Quando a aprovação sai, `GET /v1/rules` passa a devolver o conjunto novo, com `version` maior e outro `ETag`. A API não tem rota para consultar o estado de uma proposta. Acompanhe pelo painel, ou leia `GET /v1/rules` e compare `version` e `ETag`. ### Exemplos `GET /v1/rules`, resposta `200` (os valores abaixo são **ilustrativos**, não são valores de fábrica): ```http HTTP/1.1 200 OK Content-Type: application/json ETag: "" ``` ```json { "version": 4, "timezone": "America/Sao_Paulo", "currencies": { "BRL": { "auto_multiplier": "1", "deposit_min": "1500", "deposit_max": "3000000", "withdrawal_min": "2500", "withdrawal_max": "3000000", "auto_max_per_withdrawal": "400000", "auto_max_per_day": "800000", "dual_approval_above": "1500000", "max_withdrawals_per_day": 4 } }, "first_withdrawal_manual": true, "pix_key_change_cooldown_hours": 48, "require_payer_matches_player": true, "auto_hours": { "from": "08:00", "to": "22:00" } } ``` Proposta: ```http POST /v1/rules/change-requests HTTP/1.1 Host: api.example.test Content-Type: application/json; charset=utf-8 If-Match: "" Idempotency-Key: rules-2026-10-18-0001 Dracma-Key-Id: key_test_01J9EXEMPLO Dracma-Timestamp: 1790000300 Dracma-Nonce: 0123456789abcdef0123456789abcdef Dracma-Signature: v1= { "rules": { "version": 4, "timezone": "America/Sao_Paulo", "currencies": { "BRL": { "...": "o conjunto completo, com a mudança" } } }, "justification": "Reduzir o teto diário do automático durante a campanha." } ``` Resposta `202`: ```json { "id": "chg_0192a3b40c6d7e8f9a0b1c2d3e4f5a6b", "status": "pending_approval" } ``` Resposta `412` quando o `ETag` ficou velho: ```json { "type": "https://docs.dracma.test/errors/etag_mismatch", "title": "etag_mismatch", "status": 412, "code": "etag_mismatch", "trace_id": "4bf92f3577b34da6a3ce929d0e0e4736", "retryable": false } ``` ### Erros e casos de borda | Rota | Situação | Resposta | |---|---|---| | `POST` | `If-Match` ausente | `400 validation_failed`, `field: If-Match`, `reason: required` | | `POST` | corpo que não é JSON válido, ou tem campo fora de `rules` e `justification` | `400`, `field: body`, `reason: invalid_json` | | `POST` | `rules` ausente ou que não é objeto | `400`, `field: rules`, `reason: required` | | `POST` | `justification` vazia ou com mais de 1000 caracteres | `400`, `field: justification`, `reason: length` | | `POST` | o motor recusa o conjunto proposto (forma, campo desconhecido ou valor inválido) | `400`, `field: rules`, `reason: invalid` | | `POST` | `If-Match` diferente do `ETag` vigente | `412 etag_mismatch` | | `GET` e `POST` | operação sem conjunto de regras | `404 not_found` | | `GET` e `POST` | motor de regras fora | `503 service_unavailable`, repetível | | `POST` | chave sem o escopo `rules:propose` | `403 insufficient_scope` | - **`412` não é erro do seu código.** Alguém aprovou uma mudança, ou o `ETag` caducou. Releia `GET /v1/rules`, refaça a mudança sobre o conjunto novo e proponha de novo. Não repita o `POST` com o mesmo `If-Match`. - O `If-Match` é comparado como texto, com as aspas. Mande o valor exatamente como o `GET` devolveu. - A proposta registra a chave que a enviou (a operação e o autor vêm da credencial, nunca do corpo). - Os limiares de revisão dependem do conjunto. Depois de propor, **não** mude o seu código esperando o efeito: ele só existe depois da aprovação. ### Veja também - [Decisão, limites e regras](../dinheiro/decisao-limites-e-regras.md): o que cada código significa. - [Regras, listas e emergência](../painel/regras-e-listas.md): aprovação no painel. - [Limites do jogador e saldos](jogadores-e-saldos.md): a posição de um jogador. - [Erros](../api/erros.md) --- # Arquivo: docs/referencia/reconciliacao.md ## Reconciliação > Para: Dev, Fin — Conferido em 2026-10-18. `GET /v1/reconciliation` entrega, para cada dia UTC e cada moeda, a lista de movimentos liquidados, os totais e um `digest` assinado. Com ele você prova todos os dias que o seu livro e o do Dracma contam o mesmo dinheiro. Esta página descreve os parâmetros, os itens, os totais, o cálculo do digest e a verificação da assinatura. ### O que é | Rota | Escopo | `operationId` | Resposta | |---|---|---|---| | `GET /v1/reconciliation` | `read` | `reconciliacao` | `200` com uma página do relatório do dia, ou `409 day_open` | O relatório vem do **fechamento do livro** do Dracma, que é o dono do que está "liquidado". A API recalcula o digest por conta própria e o compara com o do livro: se divergirem, ela responde `503` e alerta o Dracma, em vez de entregar um relatório inconsistente. A reconciliação tem duas rotinas no seu lado: 1. **Diária:** o relatório dos movimentos liquidados em cada dia UTC, por moeda, com digest assinado (esta página). 2. **A cada 5 minutos:** `GET /v1/events?after=` para recuperar webhooks perdidos (veja [Recuperação com `GET /events`](../webhooks/recuperacao-get-events.md)). ### Quando usar Uma vez por dia, depois das **02:00 UTC** do dia seguinte, para cada moeda habilitada. O dia `D` só abre em `D+1` às 02:00 UTC (24 h mais 2 h de carência). Antes disso a resposta é `409 day_open`. | Quando | O quê | |---|---| | A cada 5 min | `GET /v1/events?after=<último evt>`: deduplique, faça o fetch-back e aplique o efeito | | Diariamente, depois das 02:00 UTC | `GET /v1/reconciliation` do dia anterior, para cada moeda: digest, assinatura e comparação com o seu livro | | Continuamente | `GET /v1/balances` para alertar caixa baixo (veja [Limites do jogador e saldos](jogadores-e-saldos.md)) | Para a visão do financeiro, no painel, veja [Reconciliação no painel](../painel/reconciliacao.md). ### Como funciona #### Parâmetros | Parâmetro | Obrigatório | Conteúdo | |---|---|---| | `date` | sim | dia **UTC**, `AAAA-MM-DD`. Formato inválido (inclusive data que não existe): `400` com `reason: format` | | `currency` | sim | moeda da carteira (`BRL`). Ausente: `400` com `reason: required`. Não habilitada: `400 currency_not_enabled` | | `cursor` | não | o `next_cursor` da página anterior. Inválido: `400` com `reason: format` | | `limit` | não | de 1 a 200. Padrão 50. Fora da faixa: `400` com `reason: range` | #### Disponibilidade e `409 day_open` Pedir um dia que ainda não abriu responde `409` com `code: day_open`: - `retryable: true`; - `Retry-After` com os **segundos** até a abertura (no mínimo 1); - `detail` com o instante de abertura: `disponível a partir de `. O erro significa "tente mais tarde". Agende a chamada para depois das 02:00 UTC e não repita em laço. #### Paginação, totais e assinatura - A ordem é a canônica: `object_id` crescente e, em empate, `kind` crescente. O `cursor` é opaco. - `totals` e `digest` só vêm na **última página**, a que traz `next_cursor: null`. Nas outras páginas os dois vêm `null`. - O cabeçalho `Dracma-Signature` também só vem na última página. - A última página **nunca** sai sem assinatura. Se o assinador, o livro ou o banco estiverem fora, a resposta é `503 service_unavailable`, repetível. - A rota usa o balde geral de leitura do rate limit. Pagine devagar mesmo assim. Veja [Limites e quotas](../api/limites.md). #### Os itens Cada item de `items` é um movimento liquidado no dia: | Campo | Tipo | Conteúdo | |---|---|---| | `object_id` | texto | o `id` do Dracma do depósito ou saque | | `external_id` | texto | o seu id | | `kind` | texto | o tipo do movimento (tabela abaixo) | | `status` | texto | o `status` do objeto naquele momento: `paid`, `refunded`, `chargeback` ou `returned` | | `amount` | `Money` | o valor do movimento | | `fee` | `Money` ou `null` | **sempre `null` hoje** | | `settled_at` | texto | RFC 3339 em UTC, **com segundos e sem fração** | **`kind`: o que o Dracma emite hoje** | `kind` | O que é | Emitido hoje? | `amount` | `settled_at` | |---|---|---|---|---| | `deposit` | depósito liquidado (`paid`) | sim | o valor pago | o `paid_at` do depósito | | `withdrawal` | saque liquidado (`paid`) | sim | o valor do saque | o `paid_at` do saque | | `refund` | estorno de depósito | só no sandbox | o valor do depósito | o instante do evento | | `chargeback` | chargeback ou disputa de depósito | só no sandbox | o valor do depósito | o instante do evento | | `return` | dinheiro devolvido: **depósito** `returned` ou **saque** `returned` | sim | depósito: o valor recebido (`paid_amount`). Saque: o `return.returned_amount` | o instante do evento | | `fee` | tarifa | **não** | — | — | | `recall`, `receipt` | recall aceito e recebimento não casado, da v1.1.0 | **não** | — | — | - `settled_at` de `deposit` e `withdrawal` é o `paid_at` do objeto. Nos demais `kind` é o instante em que o Dracma registrou o evento. Compare pelo `settled_at` do Dracma (UTC), não pela data local: perto da meia-noite UTC o mesmo movimento pode cair no dia vizinho do seu fuso. - Um depósito que foi para `returned` **sem nunca ter sido `paid`** aparece só com `return`, sem item `deposit`. Isso é normal. - Um saque devolvido aparece com **dois** itens: o `withdrawal` do dia do pagamento e o `return` do dia da devolução. - Itens `fee` e o campo `fee` do item não existem hoje: o Dracma não desconta tarifa do crédito. As tarifas são por contrato (veja [Tarifas](../dinheiro/tarifas.md)). - Aplique o **leitor tolerante**: `kind` desconhecido entra no digest como qualquer outro, mas não gera lançamento automático no seu livro. Vira item de revisão. !!! note "Reservado no contrato" `fee` (item e total), `kind: fee`, `recall` e `receipt` estão no contrato 1.1.0 e no OpenAPI, mas o Dracma não emite esses itens hoje. O total `fees` vem `"0"`. #### Os totais `totals` traz seis campos, calculados sobre **todos** os itens do dia (não só os da página): | Campo | Soma | |---|---| | `deposits_paid` | itens `deposit` | | `withdrawals_paid` | itens `withdrawal` | | `refunds` | itens `refund` | | `chargebacks` | itens `chargeback` | | `fees` | itens `fee` e `fee` dos itens. Hoje sempre `"0"` | | `count` | quantidade de itens, **inclusive** `return` | !!! warning "Os totais não incluem `return`" A API não devolve um total de `return`. O item `return` conta em `count`, mas não entra em nenhuma soma. Portanto "os totais batem com a soma dos itens" **só vale quando o dia não tem `return`**. Quando tiver, some os itens `return` à parte e compare com o seu livro. O documento OpenAPI servido também não tem esse total. #### O digest Para cada item do dia, **em todas as páginas**, monte a linha canônica: ```text object_id|external_id|kind|status|amount|currency|settled_at ``` 1. Junte os itens de todas as páginas. 2. Ordene por `object_id` crescente (comparação de bytes, sem locale). **Em empate** (o mesmo objeto em dois itens, por exemplo `withdrawal` e `return`), ordene por `kind` crescente. O contrato só cita `object_id`; o desempate por `kind` é o que o Dracma faz (veja [Divergências conhecidas](divergencias.md#reconciliacao-e-leitura)). 3. Una as linhas com `\n`. Pela definição de "unir", **não há `\n` no final**. 4. `digest = hex( sha256( linhas_unidas ) )`, em minúsculas. 5. O resultado tem de ser igual ao `digest` da última página. Detalhes que costumam errar: - `amount` é o inteiro na menor unidade como vem no JSON (`"5000"`, não `50.00`). - `currency` da linha é a moeda do item, igual a `amount.currency` e ao `currency` do topo. - `settled_at` é a string do JSON, sem reformatar. - Dia sem movimento: `items: []` e `digest` igual ao sha256 de texto vazio, `e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855`. #### A assinatura do digest ```text Dracma-Signature: t=,v1=hex( HMAC-SHA256( segredo_de_webhook, "." + digest ) ) ``` - O segredo é o **segredo de webhook** da operação (não o de chamada à API), com a **mesma rotação** do webhook. Podem vir dois `v1`, e basta um bater. - A mensagem assinada é o `t`, um ponto e o `digest` em hexadecimal (os 64 caracteres). Não é o corpo da resposta. - **Verifique na hora do download.** O Dracma assina a cada pedido da última página, então o `t=` é sempre recente. Aplique a mesma janela de ±300 s do webhook sobre o `t=`. Guarde o cabeçalho junto com o relatório como prova; uma reverificação posterior compara só o `v1`, sem a janela. - A primitiva é a mesma do webhook (`"." + mensagem`): os clientes de [Exemplos](../exemplos/README.md) reaproveitam a verificação passando o digest no lugar do corpo. Veja [Requisição e assinatura](../webhooks/requisicao-e-assinatura.md#a-assinatura). #### Fluxo completo ```mermaid sequenceDiagram participant C as Seu conector participant D as API do Dracma loop até next_cursor ser null C->>D: GET /v1/reconciliation?date=D¤cy=BRL&limit=200[&cursor=...] D-->>C: 200 com items (e, na última, totals, digest, Dracma-Signature) end Note over C,D: 409 day_open: agende para depois das 02:00 UTC e pare C->>C: junta os itens, ordena por object_id e kind C->>C: recalcula o digest e compara C->>C: verifica a assinatura (t, v1) C->>C: compara com o seu livro, divergência é incidente ``` Pseudocódigo: ```text itens = [] cursor = nulo repita: r = GET /v1/reconciliation?date=D¤cy=BRL&limit=200[&cursor=cursor] se r.status == 409 e r.code == "day_open": agendar para depois das 02:00 UTC; parar itens += r.json.items cursor = r.json.next_cursor até cursor == nulo ultima = r linhas = [f"{i.object_id}|{i.external_id}|{i.kind}|{i.status}|{i.amount.amount}|{i.amount.currency}|{i.settled_at}" para i em ordenar(itens, por=(object_id, kind))] digest = sha256_hex(juntar(linhas, "\n")) exigir digest == ultima.json.digest senão incidente sig = parse(ultima.header["Dracma-Signature"]) # t= e 1..2 v1 exigir algum v1 == HMAC_SHA256(segredo_webhook_ativo, sig.t + "." + digest) # tempo constante comparar_com_meu_livro(itens) ``` #### O que comparar com o seu livro Para cada dia e cada moeda: - Todo `deposit` do Dracma tem **exatamente um** crédito no seu livro, com o mesmo `external_id` e o mesmo valor. - Todo crédito seu tem um `deposit` no relatório. Um crédito sem par é o caso mais grave: é dinheiro criado sem liquidação. - Todo `withdrawal` corresponde a uma reserva sua finalizada, e vice-versa. - `refund`, `chargeback` e `return` correspondem às decisões que você registrou. Para `return`, veja [Reconciliar com o seu livro](../dinheiro/reconciliacao-do-operador.md). - `totals` batem com a soma dos itens, **exceto** pelo `return` (veja acima). **Divergência é incidente.** Não "conserte" o livro para bater. Abra o incidente, congele o que for preciso, investigue pelo `trace_id` e pelos eventos (`GET /v1/events?object_id=`) e registre. Dispare o alerta quando o digest não bate, quando a assinatura não bate ou quando falta um par. ### Exemplos ```http GET /v1/reconciliation?date=2026-10-17¤cy=BRL&limit=200 HTTP/1.1 Host: api.example.test Dracma-Key-Id: key_test_01J9EXEMPLO Dracma-Timestamp: 1790000400 Dracma-Nonce: 0123456789abcdef0123456789abcdef Dracma-Signature: v1= ``` Última página de um dia com um depósito, um saque e uma devolução parcial desse saque. Repare que `count` é 3 e que `totals` não tem soma de `return`: ```http HTTP/1.1 200 OK Dracma-Signature: t=1790000401,v1=<64 caracteres hexadecimais> ``` ```json { "date": "2026-10-17", "currency": "BRL", "items": [ { "object_id": "dep_0192a3b40c6d7e8f9a0b1c2d3e4f5a6b", "external_id": "dep_0192a3b4-5c6d-7e8f-9a0b-1c2d3e4f5a6b", "kind": "deposit", "status": "paid", "amount": { "amount": "5000", "currency": "BRL" }, "fee": null, "settled_at": "2026-10-17T15:07:40Z" }, { "object_id": "wdr_0192a3b40c6d7e8f9a0b1c2d3e4f5a6c", "external_id": "wdr_0192a3b4-5c6d-7e8f-9a0b-1c2d3e4f5a6c", "kind": "return", "status": "returned", "amount": { "amount": "3000", "currency": "BRL" }, "fee": null, "settled_at": "2026-10-17T23:10:00Z" }, { "object_id": "wdr_0192a3b40c6d7e8f9a0b1c2d3e4f5a6c", "external_id": "wdr_0192a3b4-5c6d-7e8f-9a0b-1c2d3e4f5a6c", "kind": "withdrawal", "status": "paid", "amount": { "amount": "5000", "currency": "BRL" }, "fee": null, "settled_at": "2026-10-17T21:30:05Z" } ], "next_cursor": null, "totals": { "deposits_paid": { "amount": "5000", "currency": "BRL" }, "withdrawals_paid": { "amount": "5000", "currency": "BRL" }, "refunds": { "amount": "0", "currency": "BRL" }, "chargebacks": { "amount": "0", "currency": "BRL" }, "fees": { "amount": "0", "currency": "BRL" }, "count": 3 }, "digest": "140a4741bd77e90442f0d09443876ec084158fe489d13e96529a21909e002d4f" } ``` As três linhas canônicas que geram esse `digest`, já na ordem certa: ```text dep_0192a3b40c6d7e8f9a0b1c2d3e4f5a6b|dep_0192a3b4-5c6d-7e8f-9a0b-1c2d3e4f5a6b|deposit|paid|5000|BRL|2026-10-17T15:07:40Z wdr_0192a3b40c6d7e8f9a0b1c2d3e4f5a6c|wdr_0192a3b4-5c6d-7e8f-9a0b-1c2d3e4f5a6c|return|returned|3000|BRL|2026-10-17T23:10:00Z wdr_0192a3b40c6d7e8f9a0b1c2d3e4f5a6c|wdr_0192a3b4-5c6d-7e8f-9a0b-1c2d3e4f5a6c|withdrawal|paid|5000|BRL|2026-10-17T21:30:05Z ``` Resposta `409` para um dia que ainda não abriu (`Retry-After: 3600`): ```json { "type": "https://docs.dracma.test/errors/day_open", "title": "day_open", "status": 409, "code": "day_open", "detail": "disponível a partir de 2026-10-18T02:00:00Z", "trace_id": "4bf92f3577b34da6a3ce929d0e0e4736", "retryable": true } ``` Dia sem movimento (`items` vazio, digest do texto vazio): ```json { "date": "2026-10-16", "currency": "BRL", "items": [], "next_cursor": null, "totals": { "deposits_paid": { "amount": "0", "currency": "BRL" }, "withdrawals_paid": { "amount": "0", "currency": "BRL" }, "refunds": { "amount": "0", "currency": "BRL" }, "chargebacks": { "amount": "0", "currency": "BRL" }, "fees": { "amount": "0", "currency": "BRL" }, "count": 0 }, "digest": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" } ``` ### Erros e casos de borda | Situação | Resposta | |---|---| | `date` inválida, `currency` ausente, `limit` ou `cursor` inválidos | `400 validation_failed`, com o campo e o `reason` em `errors[]` | | Moeda não habilitada | `400 currency_not_enabled` | | Dia ainda não aberto | `409 day_open`, `retryable: true`, `Retry-After` em segundos | | Livro, assinador ou banco fora | `503 service_unavailable`, repetível | | Digest do livro diferente do recalculado pela API | `503 service_unavailable`, e o Dracma é alertado. Nunca entrega um relatório inconsistente | | Sem o escopo `read` | `403 insufficient_scope` | - **Meia-noite UTC.** Um item do dia `D` pode ter sido creditado por você perto da meia-noite do seu fuso. Compare pelo `settled_at` (UTC). - **Digest diferente do seu.** Confira primeiro a ordenação por `(object_id, kind)`, o `\n` final e se você reformatou `settled_at`. - **Assinatura que não bate.** Confira se está usando o segredo de **webhook** e se a mensagem é `"." + digest`, não o corpo. - **Depois de uma rotação do segredo**, podem vir dois `v1` por um tempo. Basta um bater. - **`kind` novo.** Não derrube o seu job: guarde o item e abra revisão. ### Veja também - [Reconciliar com o seu livro](../dinheiro/reconciliacao-do-operador.md) - [Reconciliação no painel](../painel/reconciliacao.md) - [Recuperação com `GET /events`](../webhooks/recuperacao-get-events.md) - [Requisição e assinatura](../webhooks/requisicao-e-assinatura.md) - [Limites do jogador e saldos](jogadores-e-saldos.md) --- # Arquivo: docs/referencia/superficies-reservadas.md ## Rotas e campos reservados > Para: Dev — Conferido em 2026-10-18. O contrato 1.1.0 define câmbio, recebimentos bancários, recall de saque e transferência bancária (wire e SWIFT). O Dracma publica essas rotas no documento OpenAPI, mas **nenhuma operação tem câmbio nem trilho bancário habilitado**. Esta página mostra o que cada rota responde hoje, o que cada campo reservado vale e o que o seu código deve fazer para continuar correto. ### O que é !!! warning "Não disponível" O Dracma não oferece hoje câmbio, cotação de moeda, recebimento de transferência bancária (wire ou SWIFT) nem recall de saque. As rotas existem para manter o contrato 1.1.0 inteiro, mas respondem sempre com um erro do catálogo ou com uma lista vazia, sem criar nem alterar nada. O único método de depósito e de saque é `pix`, em `BRL`. | Rota | Escopo | `operationId` | Resposta hoje | |---|---|---|---| | `POST /v1/withdrawals/{id}/recall` | `withdrawals:write` | `pedirRecallDeSaque` | saque da operação: `409 invalid_state_transition`; saque inexistente ou de outra operação: `404`; corpo inválido: `400` | | `POST /v1/fx/quotes` | `deposits:write` ou `withdrawals:write` | `criarCotacao` | corpo válido: `422 fx_not_enabled`; corpo inválido: `400` | | `GET /v1/fx/quotes/{id}` | `read` | `obterCotacao` | sempre `404 not_found` | | `GET /v1/receipts` | `read` | `listarRecebimentos` | `200` com `{"data": [], "next_cursor": null}`; parâmetro inválido: `400` | | `GET /v1/receipts/{id}` | `read` | `obterRecebimento` | sempre `404 not_found` | | `POST /v1/sandbox/bank-transfers/incoming` | `deposits:write` | `sandboxTransferenciaRecebida` | corpo válido: `400 rail_not_enabled`; corpo inválido: `400 validation_failed`; em operação Real: `404` | Essas rotas existem desde o ajuste de 2026-10-02: antes disso respondiam `404` de rota inexistente. Veja [OpenAPI](openapi.md). ### Quando usar Na prática, **não use**. O que importa para o seu conector: - Não monte telas nem fluxos que dependam de câmbio, recibo bancário ou recall: o Dracma não os atende. - Um cliente gerado a partir do OpenAPI vai ter métodos para essas rotas. Elas existem para o contrato fechar, não para uso. - O seu código deve **tolerar** os estados, campos e eventos reservados (seção "Campos, estados e eventos reservados"). Assim, nada quebra se um dia forem preenchidos. ### Como funciona #### Recall de saque: `POST /v1/withdrawals/{id}/recall` Recall só vale para saque wire ou SWIFT já enviado. Todo saque do Dracma é PIX, então **não há caminho de sucesso**. A API confere, nesta ordem: 1. `id` fora do formato `wdr_` mais letras e números: `404 not_found`. 2. Corpo: `reason_code` é obrigatório e deve ser um destes: `duplicate`, `wrong_beneficiary`, `wrong_amount`, `fraud_suspected`, `other`. `justification` é opcional, até 1000 caracteres. Corpo fora disso: `400 validation_failed` (`enum` para o motivo, `max_length` para a justificativa, e as demais razões do corpo estrito). 3. Saque inexistente, ou de outra operação: `404 not_found`. 4. Saque existente da operação: `409 invalid_state_transition`. Nada muda no saque, qualquer que seja o estado dele. Para desfazer um saque que ainda não foi enviado, use o cancelamento descrito em [Saques](saques.md). #### Cotação de câmbio: `POST /v1/fx/quotes` e `GET /v1/fx/quotes/{id}` `POST /v1/fx/quotes` valida o corpo, confere o escopo e responde `422 fx_not_enabled`. A ordem: 1. Corpo que não é JSON válido ou tem campo desconhecido: `400 validation_failed`, `field: body`, `reason: invalid_json`. 2. Campos fora do schema, com `reason: invalid`: | `errors[0].field` | Regra | |---|---| | `purpose` | obrigatório: `deposit` ou `withdrawal` | | `sell_currency` | `sell_currency` e `buy_currency` obrigatórias e **diferentes** | | `sell_amount` | **exatamente um** entre `sell_amount` e `buy_amount`, nunca os dois nem nenhum | | `sell_amount`, `buy_amount` | texto de inteiro na menor unidade, sem zero à esquerda, até 38 dígitos | 3. Escopo conforme o `purpose`: `deposits:write` para `deposit`, `withdrawals:write` para `withdrawal`. Sem ele: `403 insufficient_scope`. 4. Corpo válido: `422 fx_not_enabled`. Nenhuma cotação é criada, de nenhuma operação. `GET /v1/fx/quotes/{id}` responde sempre `404 not_found`, com id bem ou mal formado: não há como saber se uma cotação existe. Os campos `fx_quote_id` e `payout_currency` não têm efeito. No depósito, `fx_quote_id` é recusado (`unknown_field`); no saque, `fx_quote_id`, `payout_currency` e `purpose` são aceitos pela forma e **ignorados**. Veja [Divergências conhecidas](divergencias.md). #### Recebimentos bancários: `GET /v1/receipts` e `GET /v1/receipts/{id}` Um recebimento seria um crédito bancário (wire ou SWIFT) que não casou com um depósito. Como nenhuma operação tem trilho bancário, nenhum recebimento existe. - `GET /v1/receipts` valida os parâmetros do contrato e devolve `{"data": [], "next_cursor": null}`. É uma rota de listagem e usa o balde de listagem do rate limit. - Os parâmetros são validados: `status` precisa estar em `unmatched`, `matched`, `returned`, `frozen` (`reason: enum`); `limit` de 1 a 200 (`range`); `updated_since` em RFC 3339 (`format`). - **Qualquer `cursor` é inválido**: `400` com `reason: format`, porque a API nunca emitiu um cursor de recebimento. Não é uma página vazia. - `GET /v1/receipts/{id}` responde sempre `404 not_found`, sem distinguir id bem ou mal formado. #### Transferência bancária simulada: `POST /v1/sandbox/bank-transfers/incoming` Rota de sandbox que simularia um crédito bancário entrante. Em operação **Teste** ela exige `deposits:write` e `Idempotency-Key`, valida o corpo e responde `400 rail_not_enabled`: nada é casado nem criado. Em operação **Real** ela não existe (`404`). Corpo (`amount` e `rail` são obrigatórios): | Campo | Regra | |---|---| | `amount` | `Money`: `amount` é texto de inteiro sem zero à esquerda, até 38 dígitos; `currency` é o código da moeda em maiúsculas | | `rail` | `^[a-z][a-z0-9_]{1,31}$` | | `reference` | até 140 caracteres | | `payer_name` | até 140 caracteres | | `payer_document` | `type` em `CPF`, `CNPJ`, `PASSPORT`, `NATIONAL_ID`, `TAX_ID`; `number` de 1 a 32 caracteres; `country` ISO alfa-2 | | `payer_country` | ISO alfa-2 | | `screening` | `clear` ou `hit` | Corpo fora disso: `400 validation_failed` com `reason: invalid` e o campo em `errors[0].field`. Corpo válido: `400` com `code: rail_not_enabled` e `errors[0] = {field: "rail", reason: "rail_not_enabled"}`. Nenhuma operação tem trilho habilitado, então não existe corpo que resulte em sucesso. Nem uma `reference` de um depósito seu faz diferença: a rota não lê depósito algum. ### Campos, estados e eventos reservados !!! note "Reservado no contrato" Tudo o que está nesta seção existe no documento OpenAPI e no contrato 1.1.0, mas o Dracma não preenche, não emite ou não alcança hoje. Trate como informativo e aplique o leitor tolerante. | Onde | O que | Valor hoje | |---|---|---| | `Deposito` | `rail`, `received_amount`, `receipts`, `fee`, `fees`, `fx`, `compliance`, `expected_settlement_date`, `return` | **não são devolvidos** | | `Saque` | `rail`, `fees`, `charge_bearer`, `fx`, `purpose`, `compliance`, `expected_settlement_date`, `sent_at`, `tracking`, `recall` | `null`, e `fees` é `[]` | | `Saque` | `fee` | `null`. Sem tarifa descontada do saque | | `Saque` | `return` | **preenchido** em `returned`. Não é reservado. Veja [Saques](saques.md) | | `Moeda` | `rails` | **não é devolvido** | | `Metodo` | `card`, `crypto`, `boleto`, `bank_transfer`, `wire`, `swift` | `400 method_not_enabled` | | `StatusDeposito` | `pending_compliance`, `frozen`, `paid_partial` | nenhum depósito chega a esses estados | | `StatusSaque` | `pending_compliance`, `sent`, `in_transit`, `recall_requested`, `recalled` | nenhum saque chega a esses estados; só valem como filtro de `status` | | Eventos | `deposit.pending_compliance`, `deposit.frozen`, `deposit.paid_partial`, `withdrawal.pending_compliance`, `withdrawal.sent`, `withdrawal.in_transit`, `withdrawal.recall_requested`, `withdrawal.recall_rejected`, `withdrawal.recalled`, `receipt.unmatched`, `receipt.matched`, `receipt.returned`, `receipt.frozen` | **não são emitidos**. Veja [Catálogo de eventos](../webhooks/catalogo-de-eventos.md) | | `Reconciliacao` | `kind` `fee`, `recall`, `receipt`; `fee` do item; `totals.fees` | sem itens desses `kind`; `fee` é `null`; `totals.fees` é `"0"`. Veja [Reconciliação](reconciliacao.md#os-itens) | | `Cambio`, `Compliance`, `RastreioBancario`, `Recall`, `Recebimento`, `Tarifa`, `TrilhoDisponivel` | schemas inteiros | nunca devolvidos hoje | ### Exemplos Cotação com corpo válido, resposta `422`: ```http POST /v1/fx/quotes HTTP/1.1 Host: api.example.test Content-Type: application/json; charset=utf-8 Idempotency-Key: fxq-0192a3b4-5c6d-7e8f-9a0b-1c2d3e4f5a6b Dracma-Key-Id: key_test_01J9EXEMPLO Dracma-Timestamp: 1790000000 Dracma-Nonce: 0123456789abcdef0123456789abcdef Dracma-Signature: v1= { "purpose": "deposit", "sell_currency": "USD", "buy_currency": "BRL", "sell_amount": "10000" } ``` ```json { "type": "https://docs.dracma.test/errors/fx_not_enabled", "title": "fx_not_enabled", "status": 422, "code": "fx_not_enabled", "trace_id": "4bf92f3577b34da6a3ce929d0e0e4736", "retryable": false } ``` Recall de um saque PIX da sua operação. Corpo do pedido: ```json { "reason_code": "duplicate", "justification": "Pedido duplicado por falha do meu conector." } ``` Resposta `409`: ```json { "type": "https://docs.dracma.test/errors/invalid_state_transition", "title": "invalid_state_transition", "status": 409, "code": "invalid_state_transition", "trace_id": "4bf92f3577b34da6a3ce929d0e0e4736", "retryable": false } ``` Listagem de recebimentos, resposta `200`: ```json { "data": [], "next_cursor": null } ``` Transferência simulada com corpo válido, resposta `400`: ```json { "type": "https://docs.dracma.test/errors/rail_not_enabled", "title": "rail_not_enabled", "status": 400, "code": "rail_not_enabled", "trace_id": "4bf92f3577b34da6a3ce929d0e0e4736", "retryable": false, "errors": [ { "field": "rail", "message": "trilho não habilitado na operação", "reason": "rail_not_enabled" } ] } ``` ### Erros e casos de borda | Situação | Resposta | |---|---| | Qualquer das rotas sem o escopo exigido | `403 insufficient_scope` | | `POST` sem `Idempotency-Key` | `400 validation_failed` | | Rota de sandbox em operação Real | `404 not_found` (nunca `403`) | | `422 fx_not_enabled` e `400 rail_not_enabled` | recusas definitivas: não repita | | `409 invalid_state_transition` no recall | definitiva: o recall não se aplica a saque PIX | - Uma recusa `4xx` guardada pela idempotência volta igual por 30 dias. Veja [Idempotência e repetição](../api/idempotencia.md). - `GET /v1/receipts` conta no balde de listagem: não faça polling nela. ### Veja também - [Saques](saques.md) e [Depósitos](depositos.md): o que é de fato oferecido. - [Objetos e campos](schemas.md): a coluna "devolvido hoje?". - [Catálogo de eventos](../webhooks/catalogo-de-eventos.md) - [Divergências conhecidas](divergencias.md) --- # Arquivo: docs/referencia/schemas.md ## Objetos e campos > Para: Dev — Conferido em 2026-10-18. Uma tabela por objeto da API, com tipo, nulabilidade e a coluna **"hoje"**: o que a API de fato devolve (nas respostas) ou aceita (nos pedidos). O documento OpenAPI traz mais campos do que o Dracma preenche. Esta página diz quais são os campos reais, para você escrever o seu parser sem esperar um valor que nunca vem. ### O que é A referência de cada objeto, na ordem em que aparecem nas outras páginas desta seção. Os nomes de schema (`Deposito`, `Saque`, `Moeda`...) são os do documento OpenAPI. Legenda da coluna "hoje": | Valor | Significado | |---|---| | **sim** | a API devolve (ou aceita) o campo | | **`null`** | o campo vem, mas sempre `null` (ou `[]`) | | **não** | a API nunca devolve o campo, apesar de estar no OpenAPI | | **ignorado** | a API aceita o campo no pedido e não faz nada com ele | ### Quando usar - Ao escrever o parser das respostas e os tipos do seu cliente. - Ao gerar um cliente a partir do OpenAPI: confira aqui quais campos são reais. - Ao revisar um pedido recusado com `unknown_field`: veja os campos aceitos de cada pedido. Regras gerais, que valem para todas as tabelas: - Dinheiro é sempre `Money`. Datas são RFC 3339 em UTC. IDs do Dracma são opacos e têm prefixo. Veja [Convenções HTTP](../conceitos/convencoes-http.md). - **Pedidos são estritos**: campo desconhecido vira `400 validation_failed` com `reason: unknown_field`. - **Respostas devem ser lidas com tolerância**: campo, valor de enum ou estado novo nunca devem derrubar o seu parser. ### Como funciona Uma seção por grupo de objetos: tipos comuns, pedidos, respostas de depósito e de saque, moedas e regras, reconciliação, eventos e erros. #### Tipos comuns **`Money`** | Campo | Tipo | Regra | |---|---|---| | `amount` | texto | inteiro **sem sinal e sem zero à esquerda**, na menor unidade da moeda, com até 38 dígitos. `"0"` existe, mas pedidos exigem valor maior que zero | | `currency` | texto | código da moeda. Hoje `BRL` | **`Documento`** | Campo | Tipo | Regra | |---|---|---| | `type` | texto | `CPF`, `CNPJ`, `PASSPORT`, `NATIONAL_ID` ou `TAX_ID`. Em PIX, só `CPF` e `CNPJ` passam da validação de regra | | `number` | texto | CPF/CNPJ: só dígitos, 11 a 14. Outros tipos: 1 a 32 caracteres | | `country` | texto | ISO 3166 alfa-2 em maiúsculas | **`Jogador`** (dentro de `player`, nos pedidos de depósito e saque) | Campo | Tipo | Obrigatório | Hoje | Regra | |---|---|---|---|---| | `external_ref` | texto | sim | sim | `^[A-Za-z0-9_.:-]{1,64}$`, opaco e imutável | | `document` | `Documento` | sim | sim | fixado no primeiro uso do `external_ref` | | `name` | texto | depende da operação | sim | até 120 caracteres | | `email` | texto | depende da operação | depósito: sim. Saque: **ignorado** | até 254 caracteres. Em operação Teste, só TLD reservado | | `birth_date` | texto | não | **ignorado** | informativo | | `kyc_level` | texto | não | **ignorado** | informativo | | `ip` | texto | depende | sim | IP do jogador. No saque, sem ele o saque nunca é automático | | `device_id` | texto | depende | sim | até 128 caracteres | #### Pedidos **`CriarDeposito`** (`POST /v1/deposits`). Detalhes em [Depósitos](depositos.md). | Campo | Obrigatório | Hoje | Regra | |---|---|---|---| | `external_id` | sim | sim | `^[A-Za-z0-9_.:-]{1,64}$`, único por operação | | `player` | sim | sim | `Jogador` | | `amount` | sim | sim | `Money` maior que zero | | `method` | sim | sim | só `pix` | | `expires_in_seconds` | não | sim | 300 a 86400. Padrão 1800 | | `locale` | não | sim | por exemplo `pt-BR` | | `metadata` | não | sim | até 20 chaves, valores texto | | `method_options` | não | **não** (`unknown_field`) | reservado para cartão, cripto, wire, SWIFT e boleto | | `fx_quote_id` | não | **não** (`unknown_field`) | reservado para câmbio | **`CriarSaque`** (`POST /v1/withdrawals`). Detalhes em [Saques](saques.md). | Campo | Obrigatório | Hoje | Regra | |---|---|---|---| | `external_id` | sim | sim | idem depósito | | `player` | sim | sim | `Jogador`. `player.ip` é, na prática, obrigatório | | `amount` | sim | sim | `Money` maior que zero | | `method` | sim | sim | só `pix` | | `destination` | sim | sim | exatamente um destino. Em PIX: `{"pix": {"key_type", "key"}}`; `key_type` em `cpf`, `cnpj`, `email`, `phone`, `random`; `key` de 1 a 140 caracteres | | `review_requested` | não | sim | `true` obriga revisão manual | | `review_reasons` | não | sim | lista de textos; viram `OP:` no painel | | `player_context` | não | sim | informativo; nunca libera o automático | | `metadata` | não | sim | até 20 chaves, valores texto | | `purpose`, `fx_quote_id`, `payout_currency` | não | **ignorado** | reservados para câmbio e SWIFT | #### Respostas de depósito **`Deposito`**. A API devolve 16 campos. Detalhes em [Depósitos](depositos.md). | Campo | Tipo | Nulo? | Hoje | Conteúdo | |---|---|---|---|---| | `id` | texto | não | sim | `dep_` mais 32 hexadecimais | | `external_id` | texto | não | sim | o seu id | | `status` | texto | não | sim | `StatusDeposito` | | `version` | inteiro | não | sim | a partir de 1; sobe a cada transição | | `amount` | `Money` | não | sim | valor cobrado | | `paid_amount` | `Money` | sim | sim | valor pago | | `method` | texto | não | sim | `pix` | | `player_ref` | texto | não | sim | o seu `external_ref` | | `payer` | objeto | sim | sim | `{document?, name?, matches_player}` | | `instructions` | objeto | sim | sim | `{pix: {copy_paste, expires_at, qr_code_png_base64?}}` | | `failure_code` | texto | sim | sim | motivo técnico em `failed` | | `review_reasons` | lista de texto | não | sim | `[]` fora de revisão | | `created_at`, `updated_at` | texto | não | sim | UTC, segundos, sem fração | | `paid_at`, `expires_at` | texto | sim | sim | UTC, segundos, sem fração | | `rail`, `received_amount`, `receipts`, `fee`, `fees`, `fx`, `compliance`, `expected_settlement_date`, `return` | — | — | **não** | reservados da v1.1.0 | `payer`: `document` (`Documento`), `name` (texto) e `matches_player` (booleano). Só `matches_player` é sempre presente. #### Respostas de saque **`Saque`**. Detalhes em [Saques](saques.md). | Campo | Tipo | Nulo? | Hoje | Conteúdo | |---|---|---|---|---| | `id` | texto | não | sim | `wdr_` mais 32 hexadecimais | | `external_id` | texto | não | sim | o seu id | | `status` | texto | não | sim | `StatusSaque` | | `version` | inteiro | não | sim | a partir de 1 | | `amount` | `Money` | não | sim | valor pedido | | `fee` | `Money` | sim | **`null`** | sempre `null` | | `method` | texto | não | sim | `pix` | | `player_ref` | texto | não | sim | o seu `external_ref` | | `decision` | `Decisao` | não | sim | decisão do motor | | `psp_reference` | texto | sim | sim | identificador do pagamento PIX, quando pago | | `failure_code` | texto | sim | sim | motivo técnico em `failed` | | `created_at`, `updated_at` | texto | não | sim | UTC, **pode ter fração de segundo** | | `paid_at` | texto | sim | sim | UTC, pode ter fração | | `return` | `DevolucaoBancaria` | sim | **sim** em `returned`; `null` nos demais | devolução do banco | | `rail`, `charge_bearer`, `fx`, `purpose`, `compliance`, `expected_settlement_date`, `sent_at`, `tracking`, `recall` | — | sim | **`null`** | reservados da v1.1.0 | | `fees` | lista | não | **`[]`** | reservado da v1.1.0 | **`Decisao`** | Campo | Tipo | Nulo? | Hoje | Conteúdo | |---|---|---|---|---| | `mode` | texto | não | sim | `automatic` ou `manual`. Em `requested`, `manual` | | `reasons` | lista de texto | não | sim | códigos de revisão ou bloqueio; `[]` no automático | | `ruleset_version` | inteiro | não | sim | versão do conjunto de regras usado | | `decided_by` | lista de texto | não | sim | ids opacos dos revisores; `[]` no automático | | `decided_at` | texto | sim | sim | `null` enquanto não decidido | **`DevolucaoBancaria`** (o objeto `return`) | Campo | Tipo | Nulo? | Hoje | Conteúdo | |---|---|---|---|---| | `reason_code` | texto | não | sim | código curto do PSP ou `unspecified` | | `returned_amount` | `Money` | não | sim | valor devolvido; pode ser parcial | | `returned_at` | texto | não | sim | instante da devolução | | `reason`, `return_fee` | — | — | **não** | do schema do contrato; não vêm em PIX | #### Moedas, limites, saldos e regras **`Moeda`** e **`DetalheMetodo`**. Detalhes em [Ping e moedas](ping-e-moedas.md). | Campo | Tipo | Nulo? | Hoje | Conteúdo | |---|---|---|---|---| | `code` | texto | não | sim | `BRL` | | `kind` | texto | não | sim | `fiat` | | `exponent` | inteiro | não | sim | `2` em `BRL` | | `methods` | lista de texto | não | sim | `["pix"]` | | `method_details` | lista | não | sim | um item por método | | `method_details[].method` | texto | não | sim | o método | | `method_details[].deposit_requirements` | lista de texto | não | sim | campos do `Jogador` exigidos | | `method_details[].psp_notes` | lista de texto | não | sim | avisos informativos | | `rails` | — | — | **não** | reservado da v1.1.0 | **`LimitesJogador`**. Detalhes em [Limites do jogador e saldos](jogadores-e-saldos.md). | Campo | Tipo | Hoje | |---|---|---| | `player_ref`, `currency` | texto | sim | | `total_deposited`, `total_withdrawn`, `auto_withdrawal_remaining` | `Money` | sim | | `auto_multiplier` | texto (decimal) | sim | | `blocked` | booleano | sim | **Saldo** (item de `data` em `GET /v1/balances`) | Campo | Tipo | Hoje | |---|---|---| | `available`, `pending_in`, `pending_out` | `Money` | sim | | `as_of` | texto | sim | **`ConjuntoDeRegras`**: campos principais em [Regras (API)](regras.md). A resposta de `POST /v1/rules/change-requests` tem dois campos: `id` (`chg_` mais 32 hexadecimais) e `status` (`pending_approval`). #### Reconciliação **`Reconciliacao`**. Detalhes em [Reconciliação](reconciliacao.md). | Campo | Tipo | Nulo? | Hoje | Conteúdo | |---|---|---|---|---| | `date` | texto | não | sim | `AAAA-MM-DD`, UTC | | `currency` | texto | não | sim | a moeda pedida | | `items` | lista | não | sim | itens da página | | `items[].object_id`, `external_id`, `kind`, `status` | texto | não | sim | veja a página | | `items[].amount` | `Money` | não | sim | valor do movimento | | `items[].fee` | `Money` | sim | **`null`** | sempre `null` | | `items[].settled_at` | texto | não | sim | UTC, segundos | | `next_cursor` | texto | sim | sim | `null` na última página | | `totals` | objeto | sim | sim | só na última página | | `totals.deposits_paid`, `withdrawals_paid`, `refunds`, `chargebacks`, `fees` | `Money` | não | sim | `fees` é `"0"`. **Não há total de `return`** | | `totals.count` | inteiro | não | sim | inclui os itens `return` | | `digest` | texto | sim | sim | só na última página | #### Eventos e erros **`Evento`** (envelope do webhook e item de `GET /v1/events`): `id`, `type`, `created_at`, `operation_id`, `object_id`, `object_version` e `data.object`. Os campos, formatos e o catálogo de tipos estão em [Envelope](../webhooks/envelope.md) e [Catálogo de eventos](../webhooks/catalogo-de-eventos.md). **`Problema`** (o corpo de todo erro, `application/problem+json`). Detalhes em [Erros](../api/erros.md). | Campo | Tipo | Presente | Conteúdo | |---|---|---|---| | `type` | texto | sempre | identificador do erro, no formato de uma URL. É um identificador opaco: **não** abra o endereço | | `title` | texto | sempre | igual ao `code` | | `status` | inteiro | sempre | o status HTTP | | `code` | texto | sempre | o código estável do erro | | `detail` | texto | **só quando não vazio** | por exemplo em `day_open` e `internal_error` | | `trace_id` | texto | quase sempre | informe-o ao suporte | | `retryable` | booleano | sempre | `true` quando repetir pode funcionar | | `existing` | objeto | só em `409 duplicate_external_id` | o `Deposito` ou `Saque` já existente | | `errors` | lista | só em validação | `{field, message, reason?}`; `reason` é omitido quando vazio | ### Exemplos Um erro de validação com dois itens: ```json { "type": "https://docs.dracma.test/errors/validation_failed", "title": "validation_failed", "status": 400, "code": "validation_failed", "trace_id": "4bf92f3577b34da6a3ce929d0e0e4736", "retryable": false, "errors": [ { "field": "method", "message": "Metodo desconhecido", "reason": "enum" }, { "field": "player.document.country", "message": "ISO-3166 alfa-2", "reason": "format" } ] } ``` Um `409 duplicate_external_id` traz o objeto existente: ```json { "type": "https://docs.dracma.test/errors/duplicate_external_id", "title": "duplicate_external_id", "status": 409, "code": "duplicate_external_id", "trace_id": "4bf92f3577b34da6a3ce929d0e0e4736", "retryable": false, "existing": { "id": "dep_0192a3b40c6d7e8f9a0b1c2d3e4f5a6b", "external_id": "dep_0192a3b4-5c6d-7e8f-9a0b-1c2d3e4f5a6b", "status": "pending", "version": 2 } } ``` ### Erros e casos de borda - **Campo reservado que não vem.** Não faça `payload.fee.amount` sem checar: `fee` é `null` ou nem existe. - **Campo que vem `null`.** Em `Deposito` e `Saque`, os campos de data e valor opcionais vêm `null`, não ausentes. - **`detail` ausente** não é erro: a API o omite quando está vazio. - **Enum novo.** `status`, `kind`, `reason` e `review_reasons` são listas abertas. Valor desconhecido: não derrube o parser. - **Pedido com campo a mais** volta `unknown_field`. Isso vale para `method_options` e `fx_quote_id` em depósito. - **Números grandes.** `Money.amount` pode ter 38 dígitos: use inteiro de precisão arbitrária, nunca `float`. ### Veja também - [Convenções HTTP](../conceitos/convencoes-http.md) - [Erros](../api/erros.md) - [Rotas e campos reservados](superficies-reservadas.md) - [OpenAPI](openapi.md) --- # Arquivo: docs/referencia/openapi.md ## OpenAPI > Para: Dev — Conferido em 2026-10-18. O documento OpenAPI 3.1 da API v1 é [`openapi.yaml`](openapi.yaml). Ele é uma cópia byte a byte do documento que a própria API serve, e você o baixa em `https://docs.dracma.cfd/openapi.yaml`. Esta página diz como baixá-lo, como ele se relaciona com o contrato 1.1.0 e **o que ele ainda diz de errado**, para você não gerar um cliente sobre uma informação desatualizada. ### O que é | Item | Valor | |---|---| | Versão do contrato | 1.1.0 | | Formato | OpenAPI 3.1, `application/yaml` | | Onde baixar | `https://docs.dracma.cfd/openapi.yaml` | | Quem o gera | a própria API: o arquivo é embutido no serviço e é o que ela serve em `GET /openapi.yaml` | A API implementa a rota `GET /openapi.yaml`, pública e sem autenticação, **fora** do grupo `/v1`. Mas o ponto de entrada público do Dracma só expõe `/v1/*`: qualquer outro caminho dos hosts da API responde `404`. Por isso, **não tente baixar o documento do host da API**. Use sempre o endereço do site de documentação acima. O documento não é editado por quem escreve este guia. Quando o texto da API muda, a cópia é atualizada junto. O contrato de forma do briefing (Contrato de integração v1, versão 1.1.0) fica ao lado, sem alteração, em [`contrato-1.1.0.openapi.yaml`](contrato-1.1.0.openapi.yaml), só para você ler o diff. Em conflito de **semântica**, vale o texto normativo do contrato. Para a **forma** que a API aceita e devolve hoje, vale o documento servido, com as correções desta página. ### Quando usar - Para **gerar um cliente** ou validar respostas: baixe o `openapi.yaml`. - Para saber **o que a API de fato devolve**: leia as páginas desta seção. O documento tem mais campos e rotas do que o Dracma preenche (veja [Rotas e campos reservados](superficies-reservadas.md) e [Objetos e campos](schemas.md)). - Para **achar o texto** de uma operação a partir do `operationId`: veja o [Índice de endpoints](README.md). - Para **conferir o seu cliente** contra o servido: compare o `info.version` e o conteúdo com a cópia do site, que é a que o guia usa. ### Como funciona #### Cache e `ETag` A rota da API, quando alcançável (por exemplo, em um ambiente de desenvolvimento do próprio Dracma), responde com: | Cabeçalho | Valor | |---|---| | `Content-Type` | `application/yaml; charset=utf-8` | | `ETag` | um `ETag` **forte**, derivado do hash do conteúdo | | `Cache-Control` | `public, max-age=300` | | `X-Content-Type-Options` | `nosniff` | Um pedido com `If-None-Match` igual ao `ETag` atual recebe `304`, sem corpo. `HEAD` também funciona. No site de documentação, o arquivo é um arquivo estático: baixe-o no seu CI e compare o **conteúdo** (por hash) em vez de depender de cabeçalhos. #### Como a cópia é provada - Uma checagem automática compara as duas cópias do documento com a fonte embutida na API e falha em qualquer byte diferente. Ela também falha quando a fonte não é encontrada, porque sem fonte não há prova. Antes de comparar, adultera um byte e exige que a comparação acuse. - Do lado da API, um teste exige que toda rota do roteador `/v1` esteja no documento e vice-versa, e outros testes validam as respostas reais contra os schemas. - Os exemplos JSON deste guia que representam objetos da API são validados contra o `openapi.yaml`. #### O documento servido e o contrato 1.1.0 Regra aplicada: **o contrato vence**. Quando a API divergia do contrato sem decisão registrada, a API foi corrigida. Quando a diferença é um acréscimo **aditivo** ou já tinha decisão registrada, ela fica e está listada abaixo. **Resolvidas na API (2026-10-02).** Estas rotas estavam no contrato, mas fora do documento servido: a API respondia `404` de rota inexistente. Agora estão declaradas e implementadas no recorte PIX. Nenhuma operação tem câmbio nem trilho bancário habilitado. | Rota | Antes | Agora | |---|---|---| | `GET /fx/quotes/{id}` | `404` (rota inexistente) | `404 not_found`: nenhuma cotação é emitida | | `GET /receipts` | `404` (rota inexistente) | `200 {data: [], next_cursor: null}`; parâmetro fora do schema: `400 validation_failed` | | `GET /receipts/{id}` | `404` (rota inexistente) | `404 not_found`: sem trilho bancário não existe recebimento | | `POST /withdrawals/{id}/recall` | `404` (rota inexistente) | saque da operação: `409 invalid_state_transition`; inexistente ou de outra operação: `404`; corpo inválido: `400` | | `POST /sandbox/bank-transfers/incoming` | `404` (rota inexistente) | corpo válido: `400 rail_not_enabled`; corpo inválido: `400 validation_failed`; em operação Real: `404` | **Aceitas (aditivas ou com decisão registrada).** | Diferença no documento servido | Motivo | |---|---| | `Moeda.method_details` e o schema `DetalheMetodo` | Exigências de depósito por operação. Campo aditivo | | Descrição de `Problema.errors[].reason` com motivos novos | A lista de motivos é aberta | | `404` declarado em `POST /deposits/{id}/cancel` e `POST /withdrawals/{id}/cancel` | O catálogo de erros já manda `404 not_found`, inclusive para objeto de outra operação. O documento só passou a declarar o status | | `Idempotency-Key` declarado nos `POST /sandbox/*` | A API exige a chave em todo `POST`. O contrato não declarava o cabeçalho nessas rotas | | `400` declarado em `POST /withdrawals/{id}/recall`, `GET /receipts` e `POST /sandbox/bank-transfers/incoming` | Validação de corpo e parâmetros com o catálogo de erros | | Rotas `POST /sandbox/deposits/{id}/refund` e `/chargeback` | Gatilho de sandbox para `paid → refunded` e `paid → chargeback`. Ação nova de sandbox é aditiva e responde `404` em operação Real | | Servidor de teste local removido da lista `servers` | Ele não era um servidor do Dracma, e sim um servidor de teste do lado do integrador | As divergências de **comportamento**, que o schema não mostra, estão em [Divergências conhecidas](divergencias.md). ### O que o documento servido ainda diz de errado O `openapi.yaml` é uma cópia do que a API serve. Os itens abaixo estão errados **no documento**. A correção é feita na fonte, na API, e a cópia do site só muda depois dela. Enquanto isso, vale o que esta tabela diz. !!! warning "Documento desatualizado" O documento servido não está totalmente alinhado à API. Não gere um cliente confiando cegamente nele: confira os itens abaixo. | # | Onde no documento | O que ele diz | O que vale hoje | |---|---|---|---| | 1 | `info.description` | descreve o contrato como acordo com um operador de exemplo, citado pelo nome | texto de exemplo; ignore o nome | | 2 | exemplos de e-mail do jogador | usam um domínio real de teste de um operador de exemplo | fora de produção a API só aceita e-mail de TLD reservado (`.test`, `.invalid`, `.example`) e recusa o resto com `reserved_domain_required`. Use `jogador1@operador.test` | | 3 | `servers` | `https://api.dracma.example/v1` "Produção (mTLS obrigatório)" e `https://sandbox.dracma.example/v1` | os hosts de verdade chegam no onboarding. Não existe host `sandbox.*`. mTLS é **opcional**, não obrigatório. Veja [Ambientes, tipos de operação e hosts](../conceitos/ambientes-e-hosts.md) | | 4 | `security` global e `securitySchemes.mtls` | exigem `mtls` além da assinatura, "obrigatório em produção" | só a assinatura HMAC é exigida pela API. mTLS é opcional e habilitado por instância | | 5 | resposta `401` do webhook (`webhooks.evento`) | "o Dracma NÃO insiste além da política normal e alerta" | **qualquer resposta que não seja `2xx` é reentregue** por até 72 h, inclusive `401`. O alerta é o e-mail de DLQ no fim. Veja [Retentativas, skipped e DLQ](../webhooks/retentativas-e-dlq.md) | | 6 | enum `Evento.type` | inclui `rules.updated` | o Dracma **não emite** `rules.updated`. Não espere esse evento | | 7 | `CriarDeposito.method_options` e `CriarDeposito.fx_quote_id` | campos aceitos | a API os recusa com `400 unknown_field` em depósito PIX | | 8 | `CriarSaque.payout_currency` | "exige câmbio habilitado; senão `422 fx_not_enabled`" | `purpose`, `fx_quote_id` e `payout_currency` são aceitos pela forma e ignorados | | 9 | resposta `Proibido` (`403`) | cita "IP fora da allowlist" | a lista de IPs não é aplicada às chamadas hoje. Veja [Divergências conhecidas](divergencias.md#autenticacao-transporte-e-erros) | | 10 | `GET /sandbox/webhooks`, resposta `200` | schema vazio (`type: object`) | o corpo traz o log de entregas. Os campos estão em [Sandbox](../sandbox/README.md#log-de-entregas-e-reentrega-de-webhook) | | 11 | `Reconciliacao.totals` e `Reconciliacao.items.kind` | não há total de `return`; `return` descrito como "wire/SWIFT devolvido" | o Dracma emite `return` hoje para depósito `returned` e saque `returned` (PIX), e o item conta em `count` mas não em uma soma. Veja [Reconciliação](reconciliacao.md) | | 12 | `GET /reconciliation`, resposta `409` | "a janela fecha às 00:00 UTC + 2h de carência" | o dia `D` abre em `D+1` às 02:00 UTC. O texto confunde "fecha" e "abre" | | 13 | `Deposito` | lista campos da v1.1.0 (`rail`, `fee`, `fx`, `return`...) como parte do objeto | a API nunca os devolve em depósito. Veja [Objetos e campos](schemas.md) | | 14 | ids de exemplo (`dep_01J9ZQ3`, `wdr_01J9ZQ4`, `evt_01J9ZQ5`) | ids curtos | os ids reais são o prefixo e 32 caracteres hexadecimais. Os do Dracma são opacos: não derive nada do formato | | 15 | `info.description` | diz que `GET /openapi.yaml` é "público, sem autenticação" | é verdade na API, mas o ponto de entrada público só expõe `/v1/*`. Baixe do site de documentação | ### Exemplos Baixar o documento e guardar o hash do conteúdo, para detectar mudança: ```sh curl -fsS -o openapi.yaml https://docs.dracma.cfd/openapi.yaml sha256sum openapi.yaml ``` Um pedido condicional à rota da API, onde ela for alcançável (o `ETag` abaixo é ilustrativo): ```http GET /openapi.yaml HTTP/1.1 Host: api.example.test If-None-Match: "3f8a2c1d9b7e4a5c6d0e1f2a3b4c5d6e" ``` ```http HTTP/1.1 304 Not Modified ETag: "3f8a2c1d9b7e4a5c6d0e1f2a3b4c5d6e" Cache-Control: public, max-age=300 ``` ### Erros e casos de borda - `GET /openapi.yaml` no host da API pelo ponto de entrada público responde `404`: o ponto de entrada só repassa `/v1/*`. - A rota não usa assinatura nem `Idempotency-Key`: ela fica fora do grupo `/v1`. - Mudança no documento (campos novos, rotas novas) é **aditiva** dentro de `/v1`. Veja [Política de versionamento](../versionamento/politica.md). - Um cliente gerado com um enum fechado quebra ao receber valor novo. Gere com enum aberto, ou trate o valor desconhecido como o leitor tolerante manda. - Se o `info.version` do documento subir, leia o [Changelog](../versionamento/changelog.md). ### Veja também - [Índice de endpoints](README.md) - [Divergências conhecidas](divergencias.md) - [Objetos e campos](schemas.md) - [Changelog](../versionamento/changelog.md) --- # Arquivo: docs/referencia/divergencias.md ## Divergências conhecidas > Para: Dev — Conferido em 2026-10-18. Esta página lista, sem esconder, onde o **Dracma de hoje** faz algo diferente do **Contrato v1 1.1.0**, do documento OpenAPI servido ou de outras páginas do guia. As outras páginas descrevem o que a API faz hoje e apontam para cá quando há diferença. ### O que é Cada divergência tem um id estável (`D-A1`, `D-D2`...) e três colunas: | Coluna | Conteúdo | |---|---| | Contrato diz | o que o contrato 1.1.0, o OpenAPI ou um documento irmão afirma | | Dracma faz hoje | o comportamento real, conferido em 2026-10-18 | | Seu código | o que o conector deve fazer para funcionar nos dois mundos: hoje e depois de uma correção. Quase sempre é seguir o comportamento mais restritivo | Uma divergência sai da lista, e vai para o histórico no fim, no mesmo momento em que o comportamento ou o contrato muda. ### Quando usar - Ao implementar o conector, leia a coluna "Seu código" de cada linha da sua área. - Ao abrir um chamado com o suporte ou ao comentar uma regra no código, cite o id (`D-W9`, por exemplo). - Ao ver o Dracma agir diferente do contrato, procure o assunto aqui antes de supor um defeito. ### Autenticação, transporte e erros | Id | Contrato diz | Dracma faz hoje | Seu código | |---|---|---|---| | D-A1 | §3.6: chave de uma operação com certificado de outra operação resulta em `401` | mTLS é **opcional** e, quando ligado, o certificado é verificado na borda, com uma lista de certificados confiáveis por instância. A API **não** cruza chave × certificado | nunca misture chave e certificado de operações diferentes; trate `401` como possível. Veja [Rede, allowlist e mTLS](../seguranca/rede-allowlist-e-mtls.md) | | D-A2 | 5 req/s para `/events` e `/reconciliation` (por operação) | balde de **listagem** de 5 req/s **por chave** para `GET /deposits`, `GET /withdrawals`, `GET /events` e `GET /receipts` (sem id). `GET /reconciliation` usa o balde geral de leitura | pagine `/events` e `/reconciliation` devagar (no máximo 5 req/s); faça o fetch-back sempre por id | | D-A3 | o catálogo de erros não tem `413` | corpo acima de 64 KiB resulta em `413` com `code: validation_failed` e `reason: too_large` | trate `413` como um `4xx` de validação | | D-A4 | o nonce é "32 hex" | exige **minúsculas** (`^[0-9a-f]{32}$`); maiúsculas resultam em `401 invalid_signature` | gere hexadecimal em minúsculas | | D-A5 | §4.3: `Dracma-Event-Id` diferente do `id` do corpo resulta em `400` | um documento irmão (SEGURANCA I8) diz `401`; é ele que está errado | responda `400` | | D-A6 | não consta no contrato | 20 falhas de assinatura por minuto do mesmo IP: as seguintes viram `429 rate_limited` | corrija a assinatura; nunca repita um `401` em laço | | D-A8 | `503 psp_unavailable` e `outside_operating_hours` | não são emitidos; a indisponibilidade sai como `503 service_unavailable` | trate todo `503` igual: repita com a mesma chave depois do `Retry-After` | | D-A9 | limites por jogador não constam no contrato | `POST /deposits`: 10 depósitos criados por jogador por hora. `POST /withdrawals`: 3 saques pedidos por jogador por hora. Acima disso: `429 rate_limited` com `Retry-After` | mostre "tente mais tarde" ao jogador; no saque, **mantenha a reserva** e repita com a mesma chave depois do `Retry-After` | ### Idempotência e corpo | Id | Contrato diz | Dracma faz hoje | Seu código | |---|---|---|---| | D-I1 | OpenAPI: `operation_id` no corpo "é ignorado se existir" | ignorado **só na raiz** do corpo; aninhado resulta em `400 unknown_field` | nunca envie `operation_id` | | D-I2 | chave "em processamento" | a reserva da chave dura 30 s; depois disso, outra execução pode assumir | não faça duas chamadas concorrentes com a mesma chave | ### Depósitos | Id | Contrato diz | Dracma faz hoje | Seu código | |---|---|---|---| | D-D2 | `POST /deposits/{id}/cancel` cancela o depósito | muda só o estado no Dracma. **Não** existe comando para cancelar a cobrança no PSP: o PIX continua pagável até expirar | depósito `cancelled` com `paid_amount` preenchido: não credite e trate como exceção | | D-D3 | `paid → refunded` e `paid → chargeback` | o gatilho existe **só no sandbox**: `POST /v1/sandbox/deposits/{id}/refund` e `/chargeback` (rotas aditivas; só de `paid`, outro estado resulta em `409`; emitem `deposit.refunded` e `deposit.chargeback`). Em operação Real não há gatilho | implemente o tratamento (débito por política, exceção para decisão humana) e teste-o no sandbox com essas rotas | | D-D4 | OpenAPI `CriarDeposito` tem `method_options` e `fx_quote_id` | os dois resultam em `400 unknown_field` | não envie em PIX | | D-D5 | catálogo de eventos | não existe `deposit.created`; o primeiro evento é `deposit.pending` com `object_version` 2 | não espere evento de criação | | D-D6 | §6.1 não tem transição para pagamento depois de estado terminal | pagamento em `expired`, `cancelled` ou `failed`: o estado **não muda**; `paid_amount`, `paid_at` e o pagador são gravados e o Dracma alerta | nunca credite fora de `paid`; `paid_amount` em estado terminal é exceção para decisão humana | | D-D7 | REGRAS §12 (catálogo de motivos) | emite também `RULES_UNAVAILABLE` e `RULES_INVALID_RESPONSE`, em `review_reasons` e em `decision.reasons` | leitor tolerante: motivo desconhecido não muda nada | | D-D8 | `Documento.type` aceita `PASSPORT`, `NATIONAL_ID` e `TAX_ID` | em PIX só CPF e CNPJ: os outros resultam em `422 document_invalid` (depósito) ou em bloqueio (saque) | em PIX envie só CPF ou CNPJ | ### Exigências por operação | Id | Contrato diz | Dracma faz hoje | Seu código | |---|---|---|---| | D-R1 | `player.email` e `player.name` opcionais; `Moeda` sem `method_details`; `Problema.errors[].reason` sem os motivos novos | `GET /currencies` traz `method_details[]` (`method`, `deposit_requirements`, `psp_notes`), um campo **aditivo** que está no [`openapi.yaml`](openapi.yaml) servido (schema `DetalheMetodo`) e não no contrato 1.1.0. `POST /deposits` recusa com `400` (`required_by_operation`, `format`, `max_length`, `reserved_domain_required`) antes de criar objeto. O PSP de depósito da operação define quais campos são exigidos. `player.email`, se vier, é sempre validado; `player.name` acima de 120 resulta em `400` | leia as exigências em `GET /currencies`; mande os campos; trate o `400` como recusa | | D-R2 | o corpo de depósito que o seu front monta (por exemplo `{amount, method, cpf}`) | a operação pode exigir `player.name` e `player.email` no pedido ao Dracma | o conector preenche nome e e-mail a partir do cadastro do jogador, no servidor, nunca do corpo que o front envia | | D-R3 | os exemplos do OpenAPI usam um e-mail de exemplo de um domínio real | fora de produção o Dracma só aceita e-mail de TLD reservado (`.test`, `.invalid`, `.example`); o domínio do exemplo resulta em `reserved_domain_required` | em operação Teste use `jogador1@operador.test` | ### Saques | Id | Contrato diz | Dracma faz hoje | Seu código | |---|---|---|---| | D-W1 | `approved → processing` = "enviado ao PSP" | vira `processing` no **primeiro retorno do PSP**; envio recusado antes de sair (caixa, guarda de ambiente) deixa em `approved` e alerta | `approved` pode durar; mantenha a reserva | | D-W2 | `approved → cancelled` permitido (cancelar "só antes de `processing`") | com o comando de envio já publicado, cancelar resulta em `409` (evita pagar **e** devolver) | trate `409` no cancelamento como "espere o desfecho" | | D-W3 | o diagrama §6.2 não desenha `requested → cancelled` | permitido (o texto do §6.2 e o OpenAPI permitem) | — | | D-W4 | `Decisao.mode` obrigatório | em `requested` (consulta pendente ao diretório do PIX): `mode: "manual"`, `reasons: []`, `decided_at: null` | não conclua nada de `mode` em `requested` | | D-W5 | catálogo de eventos | não existe evento público `withdrawal.requested` | — | | D-W6 | `DevolucaoBancaria` completa: `reason`, `return_fee`, `returned_amount`, `returned_at`, `reason_code` | em `returned` por PIX, o objeto `return` traz só `reason_code`, `returned_amount` e `returned_at`. `reason_code` pode ser `unspecified`, e `returned_amount` pode ser **menor** que o valor do saque (devolução parcial, que continua `returned`) | não dependa de `reason` nem de `return_fee`; use `returned_amount`. Veja [Saques](saques.md) | | D-W7 | ações de sandbox mudam o estado | `pay` e `fail` respondem `200` com o estado **atual**; o desfecho chega por evento | espere o webhook ou faça o fetch-back | | D-W8 | `player.ip` opcional | sem `player.ip` o saque **nunca** é automático: `under_review` com `OWNERSHIP_UNVERIFIED` | mande o IP do jogador em todo saque | | D-W9 | §2: `POST /withdrawals` responde `202` se o PSP demorar | sempre `201`: a criação não espera o PSP | trate `201` e `202` igual | | D-W10 | motor de regras fora: erro (implícito) | na criação do saque: `201 under_review` com `RULES_UNAVAILABLE` (na dúvida, revisão). No depósito: `503` | mantenha a reserva em `under_review` | | D-W11 | `payout_currency` diferente da moeda do valor: `422 fx_not_enabled` | `purpose`, `fx_quote_id` e `payout_currency` são aceitos e **ignorados**; o saque sai em `BRL` | não envie esses campos em PIX | ### Reconciliação e leitura | Id | Contrato diz | Dracma faz hoje | Seu código | |---|---|---|---| | D-C1 | digest: itens ordenados por `object_id` | empate (o mesmo `object_id` em dois itens) desempatado por `kind` crescente | ordene por `(object_id, kind)` | | D-C2 | — | não há vetor normativo do digest nos vetores de assinatura | use o exemplo calculado em [Reconciliação](reconciliacao.md#o-digest) para conferir o seu código | | D-C3 | `GET /rules` sem `currency` | usa a primeira moeda habilitada da operação | — | | D-C4 | `pending_in` do caixa | calculado pela API (depósitos recebidos e ainda em revisão ou retenção), não vem do livro | use só para acompanhamento | | D-C5 | `Reconciliacao.totals` cobre os movimentos do dia | não há total de `return`: o item `return` conta em `count`, mas não em nenhuma soma | some os itens `return` à parte. Veja [Reconciliação](reconciliacao.md#os-itens) | | D-C6 | `kind: return` é "wire/SWIFT devolvido ao caixa" | `return` é emitido hoje para depósito `returned` e para saque PIX `returned`. `fee`, `recall` e `receipt` não são emitidos | trate `return` como movimento de PIX; aplique o leitor tolerante nos demais | | D-C7 | tarifa em `Reconciliacao.items.fee` e `totals.fees` | `fee` do item é sempre `null` e `totals.fees` é `"0"`: o Dracma não desconta tarifa do crédito | não espere tarifa no relatório. As tarifas são por contrato | ### Webhooks | Id | Contrato diz | Dracma faz hoje | Seu código | |---|---|---|---| | D-H2 | omisso sobre redirecionamento | `3xx` conta como falha (reentrega) | responda `2xx` direto na URL cadastrada | | D-H3 | lista de IPs de saída publicada e assinada | não há lista publicada nem endpoint, e o Dracma não garante IP fixo de origem | assinatura e fetch-back são a defesa; não filtre o webhook por IP de origem | | D-H4 | o OpenAPI mostra `401` do webhook como "o Dracma não insiste" | qualquer resposta que não seja `2xx` é reentregue por até 72 h | responda `2xx` para o que já foi tratado, mesmo duplicado | | D-H5 | o OpenAPI lista `rules.updated` entre os tipos de evento | o Dracma nunca emite esse evento | não espere nem trate `rules.updated` | ### Sandbox e conformidade | Id | Contrato diz | Dracma faz hoje | Seu código | |---|---|---|---| | D-S1 | o OpenAPI do contrato 1.1.0 não declara `Idempotency-Key` nas rotas `/sandbox/*` | a chave é exigida em todo `POST`. O [`openapi.yaml`](openapi.yaml) servido a declara | mande uma chave nova em cada ação de sandbox | | D-S2 | §10: a suíte de conformidade fica numa pasta do repositório do operador de exemplo | a suíte é do Dracma e é entregue no onboarding. Veja [Sandbox](../sandbox/README.md) | — | | D-S3 | o OpenAPI do contrato 1.1.0 ([cópia fiel](contrato-1.1.0.openapi.yaml)) traz um servidor de teste local que não é do Dracma e um e-mail de exemplo de domínio real | o [`openapi.yaml`](openapi.yaml) servido não tem esse servidor; o e-mail de exemplo continua | ignore esse servidor; para o e-mail veja D-R3 | | D-S4 | a allowlist de IP vazia significa "sem acesso" | a lista de IPs da operação fica **registrada**, mas hoje **não é aplicada** às chamadas: nenhuma chamada recebe `403 ip_not_allowed` por causa dela. Em uma instância que aplique a lista: lista vazia em operação Real nega tudo, e em operação Teste aceita qualquer IP | cadastre a lista desde o Teste, mas não a trate como barreira de segurança. Veja [Rede, allowlist e mTLS](../seguranca/rede-allowlist-e-mtls.md) e [IPs do conector](../painel/allowlist-de-ip.md) | !!! warning "Não disponível" O Dracma não aplica hoje a lista de IPs às chamadas da API. O cadastro continua existindo no painel (com aprovação e espera), mas nenhuma chamada é recusada por IP. Proteja o conector com a assinatura, o escopo da chave e o segredo guardado só no servidor. ### O documento OpenAPI servido O documento que a API serve ainda traz informações erradas. A tabela completa, com a justificativa, está em [OpenAPI](openapi.md#o-que-o-documento-servido-ainda-diz-de-errado). Os itens principais: | Id | O documento diz | Vale hoje | Seu código | |---|---|---|---| | D-O1 | cita um operador de exemplo pelo nome e usa um e-mail de exemplo de domínio real (`info.description` e exemplos) | texto de exemplo; fora de produção só TLD reservado | use `jogador1@operador.test` | | D-O2 | `servers`: "Produção (mTLS obrigatório)" e um host `sandbox.*` | mTLS opcional; os hosts reais chegam no onboarding; não existe host `sandbox.*` | use o host informado no onboarding | | D-O3 | resposta `401` do webhook: "o Dracma NÃO insiste" | reentrega de qualquer não-`2xx` por até 72 h | veja D-H4 | | D-O4 | `rules.updated` no enum de eventos | nunca emitido | veja D-H5 | | D-O5 | `CriarDeposito.method_options` e `fx_quote_id` | recusados com `unknown_field` | veja D-D4 | | D-O6 | `GET /sandbox/webhooks`: resposta com schema vazio | o corpo traz o log de entregas | veja [Sandbox](../sandbox/README.md#log-de-entregas-e-reentrega-de-webhook) | | D-O7 | `Reconciliacao` sem total de `return` | o item `return` só conta em `count` | veja D-C5 | ### Resolvidas (histórico) | Id | O que era | Como ficou | |---|---|---| | D-D1 | o executor não publicava `charge.paid` quando valor ou pagador divergiam (o depósito ficava `pending`) | corrigido: o pagamento sempre chega com os valores reais e a API decide `under_review` | | D-S0 | a suíte de conformidade chamava `/sandbox/*` sem `Idempotency-Key` | corrigido: a suíte gera uma chave nova em todo `POST` | | D-H1 | §5: backoff exponencial **com jitter**; o Dracma usava intervalos fixos, sem jitter | corrigido: a reentrega soma um jitter positivo ao intervalo base (de 0 a 10 % do atraso). Veja [Retentativas, skipped e DLQ](../webhooks/retentativas-e-dlq.md) | | D-W6 (antes) | `paid → returned` quando o banco devolve: só a ação de sandbox `return` aplicava; o executor não publicava devolução real | corrigido em 2026-10-10: o Dracma consome a devolução do PSP e leva o saque pago a `returned` em operação Real, com o objeto `return`. A divergência atual de D-W6 é outra (campos da devolução) | | D-OA | rotas `fx`, `receipts`, `recall` e `bank-transfers` fora do documento servido (`404` de rota inexistente) | declaradas e implementadas no recorte PIX em 2026-10-02. Veja [Rotas e campos reservados](superficies-reservadas.md) | ### Exemplos Como citar uma divergência num chamado ao suporte: ```text Assunto: D-W9 em POST /v1/withdrawals Operação: Dracma-Trace-Id: Esperado pelo contrato: 202 quando o PSP demora. Observado: 201 com status approved. ``` Como proteger o conector nos dois mundos (D-W9, D-A8): ```text se status in (200, 201, 202): tratar como aceito e seguir pelo estado do objeto se status == 503: repetir com a mesma Idempotency-Key depois do Retry-After ``` ### Erros e casos de borda - **A divergência não está aqui.** Se o comportamento difere do guia e não há um id, abra um chamado com o `trace_id` da resposta. - **O leitor tolerante vale para todas as linhas.** Campo, estado ou motivo desconhecido nunca devem derrubar o seu conector nem mover dinheiro. - **O contrato é a referência de semântica.** Quando o texto de uma página do guia e o do contrato divergem em semântica, vale o contrato. Para a forma que a API aceita e devolve hoje, vale esta página somada às páginas da seção Referência. ### Veja também - [OpenAPI](openapi.md): o que o documento servido ainda diz de errado. - [Depósitos](depositos.md) e [Saques](saques.md) - [Reconciliação](reconciliacao.md) - [Rotas e campos reservados](superficies-reservadas.md) - [Changelog](../versionamento/changelog.md) --- # Arquivo: docs/dinheiro/README.md ## Modelo de dinheiro > Para: Fin · Dev — Conferido em 2026-10-18. O Dracma mantém, para cada operação, um caixa próprio em BRL. O depósito pago aumenta esse caixa na hora; o saque o reserva no envio e o consome quando o PIX sai. Esta página mostra quando o dinheiro se move, o que move o saldo e o efeito de cada estado no seu livro. ### O que é Três livros convivem numa operação. Só dois deles são seus de ler. | Livro | De quem é | O que registra | Você lê por | |---|---|---|---| | O seu | Você (o conector) | Carteira do jogador e reservas de saque | O seu banco de dados | | O do Dracma | Dracma, um por operação e moeda | Caixa da operação: o que entrou por depósito pago, o que está reservado e o que já saiu | `GET /v1/balances` | | O do PSP | O PSP (Mercado Pago, MaximaxPay) | Conta bancária onde o PIX entra e sai | Não é exposto | O caixa do Dracma é uma **posição contábil**, não o saldo bancário do PSP: - O depósito vira caixa quando o Dracma o marca como `paid`. O Dracma não espera nenhuma liquidação bancária adicional e não existe prazo D+N visível na API. - O Dracma nunca mostra o saldo da conta do PSP. `GET /v1/balances` descreve apenas a posição da sua operação. !!! note "Valor cheio" O crédito de um depósito `paid` é o valor cheio do depósito. O Dracma não desconta tarifa desse crédito. Veja [Tarifas](tarifas.md). ### Quando usar Leia esta página antes de: - decidir quando creditar o jogador (só em `paid`, lido pela API); - calcular quanto você pode prometer de saque (veja [Saldos e caixa livre](saldos.md)); - montar a conferência diária do seu livro com o do Dracma (veja [Reconciliar com o seu livro](reconciliacao-do-operador.md)). ### Como funciona #### Quando o dinheiro se move No depósito, o caixa cresce no instante em que o depósito chega a `paid`. No saque, o caixa é reservado quando o Dracma envia o pedido ao PSP e consumido quando o PIX é liquidado. ```mermaid sequenceDiagram autonumber participant J as Jogador participant C as Seu conector participant D as Dracma participant P as PSP C->>D: POST /v1/deposits D->>P: cria a cobrança PIX D-->>C: 201, PIX copia-e-cola (pending) J->>P: paga o PIX P-->>D: notificação de pagamento D->>P: confere o pagamento Note over D: depósito paid: o caixa da operação cresce pelo valor cheio D-->>C: deposit.paid (você lê por GET e credita o jogador) C->>D: POST /v1/withdrawals (reserva já feita no seu livro) D-->>C: 201 (approved ou under_review) Note over D: no envio ao PSP: reserva no caixa (pending_out sobe) D->>P: pede o PIX P-->>D: PIX liquidado Note over D: withdrawal paid: a reserva vira saída definitiva D-->>C: withdrawal.paid (você finaliza a sua reserva) ``` #### O que move o saldo Estes são os únicos movimentos do caixa da operação. `available` e `pending_out` seguem a coluna de cada um (a leitura de cada campo está em [Saldos e caixa livre](saldos.md)). | Movimento | Quando acontece | `available` | `pending_out` | Item no relatório diário | |---|---|---|---|---| | Depósito pago | Depósito chega a `paid` | Sobe o valor | Não muda | `deposit` | | Reserva do saque | O Dracma prepara o envio ao PSP | Não muda (o reservado continua dentro de `available`) | Sobe o valor | Nenhum | | Saque liquidado | PIX confirmado (`paid`) | Cai o valor | Cai o valor | `withdrawal` | | Reserva liberada | Envio falhou de forma definitiva (`failed`), saque `rejected` ou `cancelled` depois da reserva | Não muda | Cai o valor | Nenhum | | Saque devolvido pelo banco | `returned`: o PIX pago voltou | Sobe o valor devolvido (pode ser parcial) | Não muda | `return` | | Estorno ou chargeback de depósito | `refunded` ou `chargeback` depois de `paid` | Cai o valor | Não muda | `refund` ou `chargeback` | | Depósito devolvido em revisão | `returned` (nunca passou por `paid`) | Não muda | Não muda | `return` | Três observações: - A reserva só existe a partir do envio. Um saque em `approved` ou `under_review` ainda não consome `pending_out`; vários saques aprovados podem somar mais que o caixa livre. Quem não consegue reservar na hora do envio fica `approved` (veja [Ciclo do saque](ciclo-do-saque.md)). - Estorno e chargeback de depósito só ocorrem no sandbox hoje (veja [Devoluções, estornos e disputas](devolucoes-estornos-e-disputas.md)). - O relatório diário não tem item de tarifa nem de ajuste. #### Matriz de estados Esta é a visão única das duas máquinas: o estado, quem o causa, o efeito no seu livro, o efeito no caixa da operação e o evento de webhook. A coluna "efeito no seu livro" é a recomendação do guia; o Dracma não toca no seu livro. **Depósito** | Estado | Quem causa | Efeito no seu livro | Efeito no caixa da operação | Evento | |---|---|---|---|---| | `created` | Você, com `POST /v1/deposits` | Nenhum | Nenhum | Nenhum (o primeiro evento é `deposit.pending`) | | `pending` | Dracma, quando o PSP devolve o PIX | Nenhum; mostre o copia-e-cola | Nenhum | `deposit.pending` | | `under_review` | Dracma, quando há divergência no pagamento | Nenhum | Nenhum; o valor aparece em `pending_in` | `deposit.under_review` | | `paid` | Dracma (pagamento confere) ou revisor no painel | **Credita uma vez** pelo valor que você registrou | `available` sobe o valor | `deposit.paid` | | `expired` | Dracma, depois de `expires_at` mais uma folga | Nenhum | Nenhum | `deposit.expired` | | `cancelled` | Você, com `POST /v1/deposits/{id}/cancel` | Nenhum | Nenhum | `deposit.cancelled` | | `failed` | Dracma, quando o PSP recusa ou fica indisponível | Nenhum | Nenhum | `deposit.failed` | | `returned` | Revisor no painel, ao devolver | Nenhum | Nenhum (nunca houve crédito) | `deposit.returned` | | `refunded` | Estorno (só sandbox hoje) | Débito conforme a sua política | `available` cai o valor | `deposit.refunded` | | `chargeback` | Chargeback ou MED (só sandbox hoje) | Débito conforme a sua política | `available` cai o valor | `deposit.chargeback` | **Saque** | Estado | Quem causa | Efeito no seu livro | Efeito no caixa da operação | Evento | |---|---|---|---|---| | `requested` | Dracma, aguardando a consulta da chave PIX | Mantém a reserva | Nenhum | Nenhum | | `under_review` | Dracma (regras mandam para revisão) | Mantém a reserva | Nenhum | `withdrawal.under_review` | | `approved` | Dracma (automático) ou revisor | Mantém a reserva | Nenhum até o envio | `withdrawal.approved` | | `processing` | Dracma, no primeiro retorno do PSP | Mantém a reserva | `pending_out` com o valor reservado | `withdrawal.processing` | | `paid` | Dracma, com o PIX liquidado | **Finaliza**: a reserva vira saída definitiva | `available` e `pending_out` caem o valor | `withdrawal.paid` | | `rejected` | Dracma ou revisor | **Devolve a reserva** ao jogador | Nenhum | `withdrawal.rejected` | | `failed` | Dracma, com resposta definitiva do PSP | **Devolve a reserva** | Reserva liberada | `withdrawal.failed` | | `cancelled` | Você, o painel ou uma lista de bloqueio no envio | **Devolve a reserva** | Nenhum, ou reserva liberada | `withdrawal.cancelled` | | `returned` | Dracma, quando o banco devolve um PIX já pago | Política sua; recomendação: não mover dinheiro sozinho | `available` sobe o valor devolvido | `withdrawal.returned` | Estado ou evento desconhecido é não terminal: não mova dinheiro e confira de novo pela API. ### Exemplos Uma operação nova com um depósito de R$ 500,00 pago, um saque de R$ 100,00 liquidado e um saque de R$ 80,00 em envio: | Momento | `available` | `pending_out` | Caixa livre | |---|---|---|---| | Depois do depósito | R$ 500,00 | R$ 0,00 | R$ 500,00 | | Reserva do saque de R$ 100,00 | R$ 500,00 | R$ 100,00 | R$ 400,00 | | Saque de R$ 100,00 liquidado | R$ 400,00 | R$ 0,00 | R$ 400,00 | | Reserva do saque de R$ 80,00 | R$ 400,00 | R$ 80,00 | R$ 320,00 | ### Erros e casos de borda !!! warning "Não disponível" O Dracma não oferece aporte de caixa. O caixa da operação só cresce por depósito pago ou por devolução de saque. Não existe rota, painel ou evento para adicionar saldo de outra forma. Enquanto não houver depósitos pagos, o caixa livre é zero e um saque recebe `422 insufficient_operation_balance`. Veja [Saldos e caixa livre](saldos.md). - `GET /v1/balances` fora do ar responde `503` retryable. Repita depois do `Retry-After`; nunca decida saque com o último valor lido há muito tempo. - O caixa pode ficar negativo para novos saques depois de um estorno, chargeback ou devolução de depósito (veja [Saldos e caixa livre](saldos.md)). - Valores são `Money`: inteiro na menor unidade, sem float. Veja [Convenções HTTP](../conceitos/convencoes-http.md#dinheiro-money). ### Veja também - [Saldos e caixa livre](saldos.md) - [Ciclo do depósito](ciclo-do-deposito.md) e [Ciclo do saque](ciclo-do-saque.md) - [Reconciliar com o seu livro](reconciliacao-do-operador.md) - [Runbook de dinheiro](runbook.md) - [Estados no seu produto](../conector/estados-no-produto.md) - [Webhooks: catálogo de eventos](../webhooks/catalogo-de-eventos.md) --- # Arquivo: docs/dinheiro/fuso-dia-e-horarios.md ## Dia, fuso e horários > Para: Fin · Dev — Conferido em 2026-10-18. O Dracma usa dois relógios. O relatório de reconciliação conta o dia em UTC. As regras de saque (limite por dia, horário do automático, feriados) contam o dia no fuso da operação. Esta página diz qual relógio vale em cada caso. ### O que é | Onde | Relógio | Detalhe | |---|---|---| | Relatório `GET /v1/reconciliation` | Dia **UTC** | O dia `D` cobre `00:00:00Z` a `23:59:59Z` e abre às **02:00 UTC** de `D+1` (carência de 2 horas). | | Todos os instantes da API (`created_at`, `paid_at`, `settled_at`) | **UTC** | RFC 3339 com `Z`. Veja [Convenções HTTP](../conceitos/convencoes-http.md#datas-e-horas). | | Janela diária de saque (`windows.day`, `max_withdrawals_per_day`, `auto_max_per_day`) | **Fuso da operação** | Dia civil de `00:00` a `24:00` no fuso. | | Janela diária de depósito (`deposit_windows.day`) | **Fuso da operação** | Mesmo dia civil. | | Teto agregado do automático por hora e por dia | **Fuso da operação** | Hora civil e dia civil local. | | Horário do automático e feriados | **Fuso da operação** | Veja "Horário do automático" abaixo. | | Janelas de 7 e 30 dias, e `velocity` em minutos | **Móveis** | 7 dias = 168 horas, 30 dias = 720 horas, contadas para trás a partir do pedido. Não dependem de fuso. | O fuso da operação é o campo `timezone` do conjunto de regras (`GET /v1/rules`). Uma operação nova nasce com `America/Sao_Paulo` (UTC−03:00). !!! note "Quem define o fuso" O Dracma define o fuso ao criar a operação. Para mudá-lo, peça uma mudança de regras: ela passa pela aprovação do painel como qualquer outra mudança (veja [Decisão, limites e regras](decisao-limites-e-regras.md)). O campo `timezone` aceita um nome IANA válido, como `America/Sao_Paulo`. ### Quando usar Consulte esta página quando: - um jogador diz que "o limite do dia ainda não zerou" ou "já zerou"; - a conta do seu financeiro por dia civil não bate com o relatório do Dracma; - você quer saber por que um saque aprovado de madrugada ficou esperando. ### Como funciona 1. O motor de regras converte o instante do pedido para o fuso da operação. 2. Para as janelas "do dia", ele soma os saques e depósitos desde `00:00` local até `24:00` local, contados pelo instante em que o pedido foi criado. 3. Para as janelas de 7 e 30 dias, ele olha os últimos 168 e 720 horas, sem olhar o fuso. 4. Para o horário do automático, ele confere o dia da semana, o feriado e a faixa de horas, também no fuso local. #### Exemplo de borda entre os dois relógios Um jogador na operação com `America/Sao_Paulo` pede um saque às 22:30 do dia 26 (horário local). Em UTC são 01:30 do dia 27. | Pergunta | Resposta | |---|---| | Em que dia civil da operação o saque conta no limite diário? | Dia 26 (local). | | Em que dia UTC o item `withdrawal` aparece no relatório? | Dia 27, pelo `settled_at` do pagamento. | | Quando você pode baixar o relatório desse item? | A partir das 02:00 UTC do dia 28. | Compare o seu livro com o relatório pelo `settled_at` em UTC, nunca pela sua data local. ### Exemplos #### PIX 24x7 e horário do automático O PIX funciona 24 horas por dia, todos os dias. Não existe horário de corte bancário. O que existe é o **horário do automático**: uma regra da operação que diz quando o Dracma pode liberar saques sem revisão. Por padrão, uma operação nova tem o automático ligado todos os dias, o dia inteiro, sem calendário de feriados. Os campos que controlam isso estão em `GET /v1/rules`: | Campo | Efeito | |---|---| | `auto_hours` | Faixa `from`/`to` em `HH:MM` no fuso da operação. `null` significa o dia inteiro. Se `from` for maior que `to`, a faixa cruza a meia-noite. | | `auto_hours_ext.days` | Dias da semana em que o automático roda: `1` segunda a `7` domingo. | | `auto_hours_ext.holidays.calendar` | `null` (nenhum) ou `BR-national` (feriados nacionais). | | `auto_hours_ext.holidays.extra` | Datas extras `AAAA-MM-DD` tratadas como feriado. | | `auto_hours_ext.outside_policy` | O que fazer com um saque fora do horário: `defer` ou `review`. | O que acontece com um saque fora do horário depende de `outside_policy`: | `outside_policy` | Resultado | |---|---| | `defer` (padrão) | O saque fica `approved` e espera a próxima abertura. O Dracma reconfere periodicamente e envia quando o horário abre. | | `review` | O saque vai para `under_review` com o motivo `OUTSIDE_AUTO_HOURS`. Uma pessoa decide. | #### Feriados nacionais Com `BR-national`, o Dracma considera fora do horário nos dias 1º de janeiro, 21 de abril, 1º de maio, 7 de setembro, 12 de outubro, 2 de novembro, 15 de novembro, 20 de novembro, 25 de dezembro e na Sexta-feira Santa. Datas fora dessa lista entram por `holidays.extra`. A mudança de qualquer um desses campos segue o fluxo de mudança de regras. Veja [Regras (API)](../referencia/regras.md#propor-post-v1ruleschange-requests). ### Erros e casos de borda - **Relatório antes de 02:00 UTC.** `GET /v1/reconciliation` responde `409 day_open` com `Retry-After`. Agende a rotina para depois das 02:00 UTC e não repita em laço. - **Sem horário comum.** O Dracma tem outras verificações internas diárias. Elas rodam em horários próprios, independentes do relatório. Não espere que o relatório abra junto com elas; conte só com as 02:00 UTC de `D+1`. - **Saque `approved` parado fora do horário.** Com `outside_policy` igual a `defer`, é o esperado. Veja [Ciclo do saque](ciclo-do-saque.md). - **Motor de regras fora na hora do envio.** Um saque `approved` não é enviado sem conferência: ele espera e o Dracma tenta de novo. Nada sai sem passar pelas regras. ### Veja também - [Decisão, limites e regras](decisao-limites-e-regras.md) - [Reconciliar com o seu livro](reconciliacao-do-operador.md) - [Referência: reconciliação](../referencia/reconciliacao.md) - [Referência: regras](../referencia/regras.md) --- # Arquivo: docs/dinheiro/saldos.md ## Saldos e caixa livre > Para: Fin — Conferido em 2026-10-18. `GET /v1/balances` mostra o caixa da sua operação no Dracma. O campo que sustenta um novo saque não é `available`: é o **caixa livre**, que é `available` menos `pending_out`. Esta página explica cada campo, quando ele muda e o que acontece quando o caixa livre acaba. ### O que é | Campo | Significado | Observação | |---|---|---| | `available` | Dinheiro da operação que entrou e ainda não saiu. **Inclui o valor reservado para saques em andamento.** | Não é "o quanto você pode sacar". | | `pending_out` | Valor reservado para saques que o Dracma está enviando ao PSP. | Já está contado dentro de `available`. | | `pending_in` | Dinheiro já recebido e ainda não creditado, por depósitos em `under_review`, `pending_compliance` ou `frozen`. | Informativo. Não entra em `available`. | | `as_of` | Instante da leitura, em UTC. | Cada chamada lê o valor atual. | O **caixa livre** é: ```text caixa livre = available - pending_out ``` Ele é o que o Dracma confere antes de aceitar e de enviar um saque. Faça a mesma conta no seu lado, com inteiros na menor unidade. !!! warning "Cuidado com `available`" Quem lê `available` como saldo gastável promete saques que o caixa não cobre. Use sempre `available - pending_out`. ### Quando usar - Antes de prometer um saque ao jogador, para saber se há caixa livre para ele. - Em um alerta contínuo de caixa baixo (leia `GET /v1/balances` periodicamente, a cada poucos minutos). - Ao investigar um saque parado em `approved` por falta de caixa. ### Como funciona 1. O depósito pago aumenta `available` pelo valor cheio. `pending_out` não muda. 2. Quando o Dracma prepara o envio de um saque ao PSP, ele reserva o valor: `pending_out` sobe e `available` fica igual. O caixa livre cai. 3. Quando o PIX é liquidado, `available` e `pending_out` caem o valor do saque. O caixa livre não muda de novo (ele já tinha caído na reserva). 4. Se o envio falha de forma definitiva, a reserva é liberada: `pending_out` cai e o caixa livre volta. `pending_out` **só existe a partir do envio**. Um saque em `requested`, `under_review` ou `approved` ainda não reservou nada. Vários saques aprovados podem, juntos, somar mais que o caixa livre. Quem não consegue reservar na hora do envio fica `approved` e gera alerta no Dracma (veja [Ciclo do saque](ciclo-do-saque.md)). Leitura: - Cada moeda habilitada na operação aparece, mesmo sem movimento (zerada). - O Dracma lê o caixa no livro da operação no momento da chamada. Se o livro estiver fora, a resposta é `503` retryable. ### Exemplos Uma operação com R$ 1.500,00 em caixa, dos quais R$ 900,00 estão reservados para saques em envio, e um depósito de R$ 2.500,00 em revisão: ```json { "data": [ { "available": { "amount": "150000", "currency": "BRL" }, "pending_in": { "amount": "250000", "currency": "BRL" }, "pending_out": { "amount": "90000", "currency": "BRL" }, "as_of": "2026-10-18T15:00:00Z" } ] } ``` Caixa livre: `150000 - 90000 = 60000`, ou R$ 600,00. Um saque de R$ 700,00 pedido agora recebe `422 insufficient_operation_balance`. ### Erros e casos de borda #### Saque sem caixa livre | Situação | O que o Dracma faz | |---|---| | Na criação, o caixa livre é menor que o valor | `422 insufficient_operation_balance`. Nenhum saque é criado: devolva a reserva do jogador. Você pode tentar mais tarde com uma `Idempotency-Key` e um `external_id` novos. | | No envio, o caixa livre ficou menor que o valor | O saque continua `approved`, o Dracma alerta e tenta de novo depois. | | O Dracma não consegue ler o caixa (livro fora do ar) | O saque entra em `under_review` com o motivo `BALANCE_UNAVAILABLE`. Na dúvida, o Dracma nunca libera. | #### Caixa livre negativo O caixa livre pode ficar negativo quando um depósito já creditado é estornado ou sofre chargeback e há saques reservados. O valor sai de `available`, e `pending_out` continua igual. - Enquanto o caixa livre for negativo, todo novo saque recebe `422 insufficient_operation_balance`. - Estorno e chargeback de depósito só acontecem no sandbox hoje. Veja [Devoluções, estornos e disputas](devolucoes-estornos-e-disputas.md). - Um depósito `returned` nunca foi creditado, então não reduz o caixa. #### O Dracma não mostra o saldo no PSP !!! warning "Não disponível" O Dracma não expõe saldo por PSP nem por conexão. `GET /v1/balances` não aceita filtro de PSP, e o painel mostra apenas a posição da operação. Você nunca vê o saldo da conta bancária do PSP. #### Caixa inicial e aporte !!! warning "Não disponível" O Dracma não oferece aporte de caixa. Não há rota, painel ou transferência que adicione saldo à operação. O caixa só cresce por depósitos pagos e por devoluções de saque. Se você precisa pagar saques antes de ter depósitos, combine o procedimento com o Dracma antes do go-live. No go-live, o item "caixa inicial conferido" significa conferir o que `GET /v1/balances` mostra depois dos primeiros depósitos. Veja [Checklist de go-live](../go-live/checklist.md). #### `pending_compliance` e `frozen` `pending_in` soma o valor pago (ou, se ainda não houver, o valor pedido) dos depósitos em `under_review`, `pending_compliance` e `frozen`. Os dois últimos estados existem na máquina do depósito, mas nenhum caminho do PIX leva um depósito até eles hoje. Na prática, `pending_in` é a soma dos depósitos em revisão. Trate os dois estados como não terminais, sem efeito de dinheiro, se algum dia aparecerem. #### Faixa "Saques em verificação" Quando o painel mostra a faixa "Saques em verificação", novos saques da operação podem demorar mais que o normal. Os depósitos seguem normalmente e o caixa não muda. Veja [Ciclo do saque](ciclo-do-saque.md) e [Painel: saldos e caixa](../painel/saldos-e-caixa.md#faixa-saques-em-verificacao). ### Veja também - [Modelo de dinheiro](README.md) - [Ciclo do saque](ciclo-do-saque.md) - [Referência: limites do jogador e saldos](../referencia/jogadores-e-saldos.md) - [Painel: saldos e caixa](../painel/saldos-e-caixa.md) - [Runbook de dinheiro](runbook.md) --- # Arquivo: docs/dinheiro/ciclo-do-deposito.md ## Ciclo do depósito > Para: Dev · Fin — Conferido em 2026-10-18. Um depósito é uma cobrança PIX que o jogador paga. O Dracma só leva o depósito a `paid` quando o pagamento confere com o que você pediu, e só então o caixa da operação cresce. Esta página mostra os estados, o efeito de cada um no seu livro e o que fazer com pagamento fora do prazo, divergente ou devolvido. ### O que é O depósito passa por estas transições. `paid` é terminal para o crédito, mas ainda pode ir a `refunded` ou `chargeback`. ```mermaid stateDiagram-v2 [*] --> created created --> pending: PSP devolveu o PIX created --> failed: PSP recusou ou ficou indisponível pending --> paid: pagamento confere pending --> under_review: pagamento diverge pending --> expired: passou do prazo mais a folga pending --> cancelled: você cancelou pending --> failed under_review --> paid: revisor aceita under_review --> returned: revisor devolve paid --> refunded: estorno paid --> chargeback: chargeback ou MED ``` - Terminais: `expired`, `cancelled`, `failed`, `returned`, `refunded` e `chargeback`. - `version` começa em 1 em `created` e sobe a cada transição. Ignore qualquer estado cuja `version` seja menor ou igual à que você já aplicou. - O primeiro estado que você costuma ver é `pending`, com `version` 2. - Os estados `pending_compliance`, `frozen` e `paid_partial` fazem parte do contrato, mas nenhum caminho do PIX leva um depósito até eles hoje. Se aparecerem, não mova dinheiro e confira de novo (leitor tolerante). ### Quando usar Use esta página para decidir **quando creditar** o jogador e como tratar o que foge do caminho feliz. A regra que não muda: 1. **Só `paid` credita.** 2. Credite **uma vez**, pelo valor que você registrou na criação. Em `paid`, `paid_amount` é sempre igual a `amount`. 3. A idempotência é pelo `external_id`: dois eventos `deposit.paid` (reentrega, `GET /events`, corrida) creditam uma vez só. 4. Leia o estado pela API (`GET /v1/deposits/{id}`) antes de creditar. O corpo do webhook não autoriza crédito. ### Como funciona #### Do pedido ao pagamento ```mermaid sequenceDiagram autonumber participant C as Seu conector participant D as Dracma participant P as PSP participant J as Jogador C->>D: POST /v1/deposits (external_id, valor, jogador) D->>P: cria a cobrança P-->>D: PIX copia-e-cola e prazo D-->>C: 201 pending (ou 202 created se o PSP demorar) J->>P: paga o PIX P-->>D: notificação de pagamento D->>P: consulta o pagamento alt valor exato, pagador é o jogador, dentro do prazo D-->>C: deposit.paid C->>D: GET /v1/deposits/{id} Note over C: credita o jogador uma vez else divergência D-->>C: deposit.under_review end ``` A notificação do PSP só dispara a conferência: o Dracma consulta o PSP antes de agir. Para a decisão de `paid`, o Dracma confere quatro coisas: | Conferência | Se falhar | |---|---| | O valor pago é exatamente o valor do depósito | `under_review` com `AMOUNT_MISMATCH` | | O documento de quem pagou é o do jogador | `under_review` com `PAYER_MISMATCH` | | O PSP informou o pagador | `under_review` com `PAYER_UNKNOWN` | | O pagamento chegou até o prazo (`expires_at`) | `under_review` com `PAID_AFTER_EXPIRY` | O motor de regras também pode mandar o pagamento para revisão (por exemplo `PAYER_BLOCKED`). Se o motor estiver fora do ar ou devolver resposta incoerente, o depósito vai para revisão com `RULES_UNAVAILABLE` ou `RULES_INVALID_RESPONSE`: na dúvida, uma pessoa decide. #### Efeito no seu livro | Estado lido pela API | O que fazer no seu livro | |---|---| | `created`, `pending` | Nada. Mostre o copia-e-cola ao jogador. | | `under_review` | Nada. O dinheiro entrou no PSP, mas o crédito espera a revisão. | | `paid` | **Credite uma vez.** Antes, confira `external_id`, `amount`, `currency` e `player_ref` com o que você gravou. Se algo não bater, não credite e abra um caso (veja [Política de exceções](../conector/politica-de-excecoes.md)). | | `expired`, `cancelled`, `failed`, `returned` | Nada, mesmo que `paid_amount` ou `paid_at` estejam preenchidos. | | `refunded`, `chargeback` | Débito conforme a sua política. Recomendação: não mova dinheiro sozinho. | | Estado desconhecido | Nada. Trate como não terminal e confira depois. | Quando o depósito chega a `paid`, o caixa da operação sobe o valor cheio na hora. Não há tarifa descontada do crédito (veja [Tarifas](tarifas.md)). #### Revisão: quem decide e como `Deposito.review_reasons` lista por que o depósito está em `under_review`. A decisão é de um revisor no painel, que registra o motivo (veja [Painel: revisão](../painel/revisao-de-depositos-e-saques.md#decidir-um-item)). | Motivo | Quando aparece | Saída usual | |---|---|---| | `PAYER_MISMATCH` | Quem pagou não é o jogador (inclui CNPJ pagando por pessoa física) | Devolver | | `PAYER_UNKNOWN` | O PSP não informou o pagador | Revisão humana | | `PAYER_BLOCKED` | O documento do pagador está em lista de bloqueio | Devolver | | `AMOUNT_MISMATCH` | O valor pago é diferente do cobrado | Devolver (não existe crédito parcial em PIX) | | `PAID_AFTER_EXPIRY` | O pagamento chegou depois de `expires_at` | Aprovar ou devolver | | `RULES_UNAVAILABLE`, `RULES_INVALID_RESPONSE` | O motor de regras falhou na hora do pagamento | Revisão humana | As duas decisões possíveis: - **Aceitar:** `under_review` vai a `paid`. O Dracma grava `paid_amount` igual a `amount` se estiver vazio e libera o crédito do caixa. **`AMOUNT_MISMATCH` não pode ser aceito**: o único caminho é devolver. - **Devolver:** `under_review` vai a `returned`. O campo `payer` (em `paid` e `under_review`) traz `document`, `name` e `matches_player`. Use-o só para exibir e auditar; a decisão de crédito é o `status`. ```json { "id": "dep_0192a3b40c6d7e8f9a0b1c2d3e4f5a6b", "external_id": "dep_0192a3b4-5c6d-7e8f-9a0b-1c2d3e4f5a6b", "status": "under_review", "version": 3, "amount": { "amount": "5000", "currency": "BRL" }, "paid_amount": { "amount": "4000", "currency": "BRL" }, "method": "pix", "player_ref": "0192a3b4-0000-7000-8000-000000000001", "payer": { "document": { "type": "CPF", "number": "12345678909", "country": "BR" }, "name": "Jogador Teste", "matches_player": true }, "instructions": null, "failure_code": null, "review_reasons": ["AMOUNT_MISMATCH"], "created_at": "2026-10-18T15:00:00Z", "updated_at": "2026-10-18T15:04:10Z", "paid_at": "2026-10-18T15:04:09Z", "expires_at": "2026-10-18T15:30:00Z" } ``` #### `returned`: o que significa `returned` é o desfecho em que o revisor recusou o crédito. Para o seu livro, o efeito é nenhum: o depósito nunca foi creditado e o caixa da operação não muda. O relatório diário lista o depósito como item `return` (veja [Reconciliar com o seu livro](reconciliacao-do-operador.md)). !!! warning "Não disponível" A API não tem operação para devolver o dinheiro ao pagador. `returned` marca o estado do depósito no Dracma; este guia não descreve como o valor volta ao pagador. Se o pagador reclamar, abra um chamado com o `id` do depósito (veja [Suporte](../ajuda/suporte.md)). #### Expiração - `expires_in_seconds` aceita de 300 a 86400. O padrão é 1800 (30 minutos). - O Dracma expira o depósito `pending` **2 minutos depois** de `expires_at`. A folga existe porque o PSP ainda pode aceitar um pagamento logo depois do prazo. Um job a cada 30 segundos faz a varredura, e então sai `deposit.expired`. - Mostre o copia-e-cola ao jogador até `instructions.pix.expires_at`, que é o prazo que o PSP aplica. #### Prazo por PSP Cada PSP aplica o próprio limite de prazo da cobrança: | PSP | Prazo efetivo da cobrança | |---|---| | Mercado Pago | Entre 31 minutos e cerca de 30 dias. Um prazo menor é elevado ao mínimo; um maior é reduzido ao máximo. | | MaximaxPay | O prazo é o do PSP; confirme com o Dracma no onboarding. | Quando o PSP muda o prazo, o valor que vale para o jogador vem em `instructions.pix.expires_at`. O campo `expires_at` do depósito guarda o prazo que você pediu e é com ele que o Dracma decide expirar. Com Mercado Pago, não peça menos de 31 minutos (1860 segundos): com um prazo curto, o PSP mantém a cobrança pagável por mais tempo que o Dracma, e um pagamento nessa janela cai em "pagamento tardio" (abaixo). ### Exemplos Pagamento tardio: o PIX foi pago depois de o depósito ter expirado. O estado **não muda**; o Dracma grava o que recebeu. ```json { "id": "dep_0192a3b40c6d7e8f9a0b1c2d3e4f5a6b", "external_id": "dep_0192a3b4-5c6d-7e8f-9a0b-1c2d3e4f5a6b", "status": "expired", "version": 3, "amount": { "amount": "5000", "currency": "BRL" }, "paid_amount": { "amount": "5000", "currency": "BRL" }, "method": "pix", "player_ref": "0192a3b4-0000-7000-8000-000000000001", "payer": { "document": { "type": "CPF", "number": "12345678909", "country": "BR" }, "name": "Jogador Teste", "matches_player": true }, "instructions": null, "failure_code": null, "review_reasons": [], "created_at": "2026-10-18T15:00:00Z", "updated_at": "2026-10-18T15:40:00Z", "paid_at": "2026-10-18T15:39:00Z", "expires_at": "2026-10-18T15:30:00Z" } ``` Regra de leitura: `status` é `expired`, mas `paid_amount` e `paid_at` estão preenchidos. **Não credite** e abra um caso `needs_decision`. ### Erros e casos de borda #### Pagamento depois do prazo ou do cancelamento | Quando o pagamento chega | O que o Dracma faz | |---|---| | Com o depósito `pending`, mas depois de `expires_at` | Vai para `under_review` com `PAID_AFTER_EXPIRY`. | | Com o depósito já `expired`, `cancelled` ou `failed` | **O estado não muda.** Grava `paid_amount`, `paid_at` e o pagador, marca para reconciliação e dispara um alerta interno no Dracma. | Para você, no segundo caso: não credite (o estado é terminal sem crédito) e abra um caso em `needs_decision` ao ver `paid_amount` num depósito terminal. #### Cancelar não cancela no PSP `POST /v1/deposits/{id}/cancel` só vale em `pending` e muda apenas o estado no Dracma. O PIX continua pagável até o prazo do PSP. Um pagamento depois disso cai na regra do pagamento tardio. Em `created`, espere `pending` e cancele depois; em qualquer outro estado a resposta é `409 invalid_state_transition`. #### Falha na criação `failed` com `failure_code` mostra o motivo técnico: `psp_unavailable` (o PSP não respondeu), `psp_rejected` (o PSP recusou) ou `environment_guard` (o PSP devolveu algo que o Dracma recusa entregar). É uma lista aberta; o `status` é que manda. #### Estorno e chargeback A máquina e o caixa já tratam `refunded` e `chargeback`, mas hoje o gatilho existe só no sandbox. Veja [Devoluções, estornos e disputas](devolucoes-estornos-e-disputas.md). ### Veja também - [Modelo de dinheiro](README.md) - [Saldos e caixa livre](saldos.md) - [Referência: depósitos](../referencia/depositos.md) - [Política de exceções de dinheiro](../conector/politica-de-excecoes.md) - [Estados no seu produto](../conector/estados-no-produto.md) - [Painel: revisão de depósitos e saques](../painel/revisao-de-depositos-e-saques.md) --- # Arquivo: docs/dinheiro/ciclo-do-saque.md ## Ciclo do saque > Para: Dev · Fin — Conferido em 2026-10-18. Um saque é um pedido de pagamento PIX ao jogador. O Dracma decide se ele sai no automático ou vai para revisão, reserva o caixa no envio e só confirma `paid` quando o PIX é liquidado. Esta página mostra os estados, o que pode demorar, o que pode ficar parado e o que o seu livro deve fazer em cada caso. ### O que é ```mermaid stateDiagram-v2 [*] --> requested: chave PIX em consulta [*] --> approved: regras aprovam [*] --> under_review: regras pedem revisão requested --> approved requested --> under_review requested --> rejected: titular da chave diverge requested --> cancelled under_review --> approved: revisor aprova under_review --> rejected under_review --> cancelled approved --> processing: primeiro retorno do PSP approved --> cancelled: antes de o envio sair processing --> paid processing --> failed paid --> returned: banco devolveu ``` - O saque nasce já decidido (`approved` ou `under_review`) ou em `requested`, quando a consulta da chave PIX ainda está pendente. A criação responde sempre `201` e não espera o PSP. - `version` sobe a cada transição. Ignore estado com `version` menor ou igual ao já aplicado. - `rejected`, `failed`, `cancelled` e `returned` são terminais; `paid` só pode ir a `returned`. - Os estados `pending_compliance`, `sent`, `in_transit`, `recall_requested` e `recalled` fazem parte do contrato, mas não ocorrem em PIX hoje. Se aparecerem, mantenha a reserva e confira de novo. ### Quando usar Consulte esta página para: - definir o prazo de suporte de um saque em `processing`; - explicar ao financeiro por que um saque `approved` não saiu; - decidir o que fazer com a reserva do jogador em cada estado. A regra de ouro está na [Referência: saques](../referencia/saques.md): **reserve o valor no seu livro antes de pedir o saque** e só devolva a reserva com resposta conclusiva (4xx síncrono, ou estado `rejected`, `failed` ou `cancelled` lido pela API). Timeout, erro de rede, `5xx` e `429` não são respostas conclusivas. ### Como funciona #### Efeito no seu livro | Estado lido pela API | Reserva no seu livro | Observação | |---|---|---| | `requested` | Mantém | A consulta da chave PIX pode levar até cerca de 5 minutos. | | `under_review` | Mantém | Um revisor decide; os motivos estão em `decision.reasons`. | | `approved` | Mantém | Aprovado, mas ainda não enviado. Veja "`approved` parado". | | `processing` | Mantém | O pedido foi enviado ao PSP. Veja "`processing` pode durar". | | `paid` | **Finaliza**: vira saída definitiva | `psp_reference` traz o identificador ponta a ponta do PIX. | | `rejected` | **Devolve** ao jogador | Motivo em `decision.reasons`. | | `failed` | **Devolve** ao jogador | O dinheiro **não saiu**. Motivo em `failure_code`. | | `cancelled` | **Devolve** ao jogador | Cancelado por você, pelo painel ou por lista de bloqueio no envio. | | `returned` (depois de `paid`) | Política sua | Veja "Banco devolveu o PIX". | | Estado desconhecido | Mantém | Não terminal; confira depois. | Devolver e finalizar são idempotentes pelo `external_id`: dois eventos do mesmo desfecho movem o dinheiro uma vez. #### `processing` pode durar `processing` significa que o Dracma enviou o pedido ao PSP e espera o desfecho. Dentro dele cabem situações diferentes: o PSP aceitou e ainda não liquidou, o PSP não respondeu de forma conclusiva, ou o caso passou para tratamento manual do Dracma. Quando o PSP não dá resposta conclusiva, o Dracma consulta o PSP repetidas vezes, em intervalos crescentes, por até cerca de 1 h 30. Se mesmo assim não houver resposta, o caso passa para tratamento manual pelo time do Dracma, que também é avisado por alerta. Nessa hora o saque **continua `processing`**. Regras que valem sempre: - **Nunca há reenvio.** O Dracma não manda o mesmo pedido de novo ao PSP, para não pagar duas vezes. - **Nunca há failover de PSP.** Se o PSP de saque não responde, o Dracma não tenta outro. - **`failed` só sai com resposta definitiva do PSP** de que o dinheiro não saiu. Se você vir `failed`, a reserva pode voltar ao jogador. - **Enquanto estiver `processing`, mantenha a reserva.** Não devolva, não reenvie e não crie outro saque. Defina um prazo de suporte pelo seu lado. Como o Dracma consulta o PSP por até cerca de 1 h 30 antes de passar o caso para tratamento manual, um saque em `processing` por mais de 2 horas merece um chamado com o `id` do saque e o `trace_id` (veja [Suporte](../ajuda/suporte.md)). #### `approved` parado `approved` não quer dizer "saiu". O saque pode ficar aprovado e parado por quatro motivos: | Causa | O que acontece | O que fazer | |---|---|---| | Fora do horário do automático | O Dracma reconfere periodicamente e envia quando o horário abre. Veja [Dia, fuso e horários](fuso-dia-e-horarios.md). | Nada. | | Caixa livre menor que o valor | O Dracma reconfere periodicamente e alerta. O saque sai quando houver caixa. Veja [Saldos e caixa livre](saldos.md). | Aumentar o caixa por depósitos ou cancelar o saque. | | Saques em verificação na operação | O painel mostra a faixa neutra "Saques em verificação". Novos saques esperam em `approved` e saem quando a verificação termina. Os depósitos seguem normalmente. | Aguardar. Em dúvida, chame o suporte. | | Envio recusado antes de sair | A reserva de caixa foi recusada na hora do envio, ou o ambiente não permite o envio. Nada foi enviado ao PSP. O saque fica `approved` e o Dracma alerta. | Veja "Reserva recusada no envio". | Dois detalhes a mais: - Um saque **sem `player.ip` nunca é enviado**. O PSP de saque exige o IP do titular; sem o IP o saque nasce em `under_review` com `OWNERSHIP_UNVERIFIED`, e, se um revisor o aprovar, ele fica `approved` sem sair. - Uma lista de bloqueio atualizada antes do envio leva o saque a `cancelled` com `BLOCKLIST_HIT_AT_DISPATCH`. !!! note "Saques em verificação" A faixa "Saques em verificação" é intencionalmente neutra: ela não mostra motivo, conta ou prazo. O efeito é só o descrito acima. O Dracma não publica prazo para o fim da verificação. Peça informação pelo canal de suporte combinado no onboarding. #### Reserva recusada no envio Se o Dracma não consegue reservar o caixa na hora do envio (por exemplo, o caixa livre baixou depois da aprovação), a tentativa termina sem enviar nada e o saque fica `approved` com alerta interno. **Esse caso não é reenviado sozinho.** Você tem dois caminhos: 1. Cancelar o saque com `POST /v1/withdrawals/{id}/cancel`. Como o envio não saiu, a resposta é `200` com `status: "cancelled"`. Devolva a reserva do jogador ao ler `cancelled`. 2. Abrir um chamado com o `id` do saque para o Dracma investigar e orientar (veja [Suporte](../ajuda/suporte.md)). Monitore o caixa livre (veja [Saldos e caixa livre](saldos.md)) e alerte antes de ele acabar. Assim este caso quase não acontece. #### Cancelamento `POST /v1/withdrawals/{id}/cancel` é permitido em `requested`, `under_review` e `approved`, **enquanto o comando de envio ao PSP não saiu**. A resposta é `200` com o objeto em `cancelled` (ou o mesmo `200` se já estava cancelado). Depois que o envio saiu, ou em `processing` e depois, a resposta é `409 invalid_state_transition`: o dinheiro pode estar saindo. Espere o desfecho (`paid` ou `failed`). Um saque que espera a verificação da operação já teve o envio pedido e também responde `409`. #### Consulta da chave PIX Quando a consulta da titularidade da chave PIX está indisponível, o saque é criado em `requested`. O Dracma reconsulta com intervalos crescentes por até cerca de 5 minutos e então decide: `approved`, `rejected` se o titular diverge, ou `under_review` com `DICT_UNAVAILABLE`. #### `failure_code` `failure_code` é uma lista aberta; o `status` é que manda. Os valores que o Dracma produz: | `failure_code` | Significado | |---|---| | `psp_failed` | O PSP recusou o pagamento (valor padrão quando o PSP não informou motivo). | | `psp_` | O PSP informou o motivo em código curto, por exemplo `psp_rejected`. | | `psp_not_found` | O PSP respondeu de forma definitiva que não conhece o pedido. | | `psp_unsupported` | O PSP da conexão escolhida não faz saque. Falha **antes** de qualquer envio. | Em todos os casos o dinheiro não saiu. Os códigos `environment_guard` e `reservation_refused` não geram `failed`: o saque fica `approved` (veja acima). #### Banco devolveu o PIX O Dracma sabe tratar a devolução de um PIX de saque já pago. O mecanismo: 1. O PSP avisa que o banco devolveu o PIX. 2. O aviso só dispara a conferência: o Dracma consulta o PSP e só age se a consulta confirmar a devolução. 3. O saque vai de `paid` para `returned` e sai `withdrawal.returned`. 4. O objeto `return` passa a vir preenchido. A devolução pode ser **parcial**: o saque continua `returned`, e o caixa recebe só o valor devolvido. 5. O caixa da operação sobe o valor devolvido (`available`), e o relatório diário ganha um item `return` no dia da devolução. Campos de `return`: | Campo | Conteúdo | |---|---| | `reason_code` | Código curto do PSP, ou `unspecified` se o PSP não informou. | | `returned_amount` | `Money` com o valor devolvido. Nunca maior que o valor pago. | | `returned_at` | Instante da devolução, em UTC. | O Dracma é idempotente nesse ponto: uma segunda devolução do mesmo saque não credita o caixa de novo. Devolução com valor zero, maior que o pago ou em outra moeda é rejeitada e gera alerta, sem lançar nada. !!! note "Quais PSPs informam devolução" O mecanismo e o objeto `return` estão prontos. O guia não afirma quais PSPs informam devoluções hoje. Planeje o seu conector para tratar `returned` e confirme o comportamento da sua operação no onboarding. ```json { "id": "wdr_0192a3b40c6d7e8f9a0b1c2d3e4f5a6c", "external_id": "wdr_0192a3b4-5c6d-7e8f-9a0b-1c2d3e4f5a6c", "status": "returned", "version": 6, "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:01Z" }, "psp_reference": "E00000000202610181500AbCdEfGhIjKl", "failure_code": null, "created_at": "2026-10-18T15:00:00Z", "updated_at": "2026-10-19T10:00:00Z", "paid_at": "2026-10-18T15:00:09Z", "return": { "reason_code": "unspecified", "returned_amount": { "amount": "5000", "currency": "BRL" }, "returned_at": "2026-10-19T10:00:00Z" } } ``` ### Exemplos Linha do tempo de um saque automático pago: | Instante (UTC) | Evento | `status` | Caixa da operação | |---|---|---|---| | 15:00:00 | `POST /v1/withdrawals` | `approved` (resposta `201`) | Sem mudança | | 15:00:02 | Envio ao PSP | `approved` | `pending_out` sobe o valor | | 15:00:03 | Primeiro retorno do PSP | `processing` | Sem mudança | | 15:00:09 | PIX liquidado | `paid` | `available` e `pending_out` caem o valor | ### Erros e casos de borda - **Operação Real sem PSP de saque.** O Mercado Pago só faz depósito. Se a operação Real não tiver um PSP de saque configurado, o Dracma não encontra por onde pagar: o envio é recusado antes de sair, o saque fica `approved` e o Dracma alerta. Confirme no onboarding que a sua operação tem PSP de saque (veja [PSPs](psps-e-ambientes.md)). - **Sem failover.** Uma operação tem um PSP de saque por vez. Se ele ficar indisponível, os saques esperam; o Dracma não troca de PSP no meio do caminho. - **`returned` e o seu livro.** Veja [Política de exceções de dinheiro](../conector/politica-de-excecoes.md): a recomendação é abrir um caso, não recreditar sozinho. ### Veja também - [Referência: saques](../referencia/saques.md) - [Modelo de dinheiro](README.md) - [Decisão, limites e regras](decisao-limites-e-regras.md) - [Devoluções, estornos e disputas](devolucoes-estornos-e-disputas.md) - [Estados no seu produto](../conector/estados-no-produto.md) - [Runbook de dinheiro](runbook.md) --- # Arquivo: docs/dinheiro/decisao-limites-e-regras.md ## Decisão, limites e regras > Para: Dev · Fin — Conferido em 2026-10-18. Toda criação de depósito e de saque passa pelo motor de regras da sua operação. O resultado é um de três: **automático** (segue), **revisão** (uma pessoa decide) ou **bloqueio** (recusa na hora, com `422`). Esta página explica a ordem da decisão, o catálogo de motivos (`decision.reasons`) e cada família de limite. Os valores dos limites são da sua operação: leia-os em `GET /v1/rules`. ### O que é | Resultado | Quando | O que você recebe | |---|---|---| | Automático | Nenhuma regra de bloqueio nem de revisão disparou, e o horário permite | Saque `approved` (segue para o envio) ou depósito criado normalmente | | Revisão | Alguma regra de revisão disparou | Saque `under_review` com `decision.reasons`; depósito pago `under_review` com `review_reasons` | | Bloqueio | Alguma regra de bloqueio disparou | `422` sem criar objeto. Devolva a reserva ao jogador | Um quarto resultado existe só no saque: **aguardar**. O saque é aprovado, mas está fora do horário do automático (`outside_policy` igual a `defer`). Ele fica `approved` e espera a abertura (veja [Dia, fuso e horários](fuso-dia-e-horarios.md)). ### Quando usar Use esta página para: - traduzir `decision.reasons` em mensagem de suporte (nunca ao jogador); - decidir o que pedir ao Dracma quando os limites da operação ficarem apertados ou frouxos; - montar a pré-checagem do seu conector, que só pode endurecer a decisão. Nunca revele ao jogador o motivo de uma revisão ou bloqueio: o suporte vê, o jogador não. Mostre uma mensagem genérica. ### Como funciona #### Ordem da decisão ```mermaid flowchart TD A["Pedido de saque"] --> B{"Regra de bloqueio?
valor, documento, listas,
titular, janela dura, caixa"} B -->|"sim, a primeira que bater"| C["422, sem objeto"] B -->|"não"| D["Avalia TODAS as regras de revisão"] D --> E{"Algum motivo?"} E -->|"sim"| F["under_review
com todos os motivos"] E -->|"não"| G{"Dentro do horário
do automático?"} G -->|"sim"| H["approved, envia"] G -->|"não, defer"| I["approved, espera abrir"] ``` - As regras de bloqueio rodam em ordem fixa e **param na primeira**: o `422` traz um único `code`. - As regras de revisão rodam **todas**: `decision.reasons` traz todos os motivos que se aplicam, sem repetir, na ordem do catálogo. - Se um valor igual ou acima do limite de dupla aprovação (ou um motivo que exija duas pessoas) aparecer, entra `DUAL_APPROVAL_REQUIRED` e a revisão precisa de **dois** revisores. Caso contrário, basta um. - O depósito segue um caminho mais curto: valor mínimo e máximo, documento, lista de bloqueio, janela de depósitos e depósitos não pagos. As revisões do depósito acontecem quando o pagamento chega (veja [Ciclo do depósito](ciclo-do-deposito.md)). #### Recusas síncronas (422) | `code` do `422` | Motivo interno | Quando | |---|---|---| | `amount_below_minimum` | `AMOUNT_BELOW_MINIMUM` | Abaixo do mínimo da operação | | `amount_above_maximum` | `AMOUNT_ABOVE_MAXIMUM` | Acima do máximo da operação | | `document_invalid` | `DOCUMENT_INVALID` | CPF ou CNPJ com dígito errado | | `document_mismatch` | `PLAYER_DOCUMENT_CHANGED`, `DOCUMENT_MISMATCH` | Documento diferente do já fixado para o jogador, ou chave PIX de outro titular | | `player_blocked` | `PLAYER_BLOCKED` | Jogador (documento, referência, IP ou device) em lista de bloqueio | | `destination_blocked` | `DESTINATION_BLOCKED` | Chave PIX em lista de bloqueio | | `velocity_exceeded` | `VELOCITY_HARD_LIMIT`, `WINDOW_HARD_LIMIT`, `DEPOSIT_WINDOW_EXCEEDED`, `DEPOSIT_VELOCITY_UNPAID` | Limite duro de janela ou de velocidade, no saque ou no depósito | | `insufficient_operation_balance` | `INSUFFICIENT_OPERATION_BALANCE` | O caixa livre da operação não cobre o saque | Nunca revele ao jogador que ele está em uma lista. A mesma `Idempotency-Key` repete o mesmo `422`; para tentar de novo, use `external_id` e chave novos (veja [Idempotência](../api/idempotencia.md#a-chave)). #### Catálogo de `decision.reasons` Os códigos são estáveis e só crescem. Um código desconhecido deve ser tratado 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 zero) | | `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 na operação: sempre humano, se a operação assim configurou | | `OPERATOR_REVIEW_REQUESTED` | R | Você enviou `review_requested: true` ou `review_reasons` | | `OWNERSHIP_UNVERIFIED` | R | Titularidade da chave não confirmada. Inclui saque sem `player.ip` | | `DICT_UNAVAILABLE` | R | Consulta da chave PIX indisponível por mais de cerca de 5 minutos | | `BALANCE_UNAVAILABLE` | R | O caixa da operação não pôde ser lido (livro fora do ar): na dúvida, revisão | | `RULES_UNAVAILABLE`, `RULES_INVALID_RESPONSE` | R | O motor de regras estava fora ou devolveu resposta incoerente: na dúvida, revisão | | `WINDOW_DAY_AMOUNT_EXCEEDED`, `WINDOW_DAY_COUNT_EXCEEDED` | R | Soma ou quantidade de saques do jogador no dia civil passou do limiar de revisão | | `WINDOW_7D_AMOUNT_EXCEEDED`, `WINDOW_7D_COUNT_EXCEEDED` | R | Idem, na janela móvel de 7 dias | | `WINDOW_30D_AMOUNT_EXCEEDED`, `WINDOW_30D_COUNT_EXCEEDED` | R | Idem, na janela móvel de 30 dias | | `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 que não são PIX | | `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 aparece em mais de uma referência de jogador | | `IP_LISTED`, `DEVICE_LISTED` | R | IP ou device do jogador em lista de revisão | | `VIP_LIMIT_EXCEEDED` | R | Jogador VIP passou do limite próprio | | `OPERATION_AUTO_CAP_EXCEEDED` | R | Teto agregado de saques automáticos da operação atingido | | `OUTSIDE_AUTO_HOURS` | R | Fora do horário do automático, com política `review` | | `DUAL_APPROVAL_REQUIRED` | R | Valor ou motivo que exige duas pessoas | | `AML_STRUCTURING` | R | Fracionamento de valores que lembra evasão de limite de reporte | | `AML_RAPID_TURNAROUND` | R | Saque logo depois de um depósito, de forma repetida | | `AML_NO_PLAY_REPORTED` | R | Você informou resultado de jogo igual a zero com depósitos | | `AML_SHARED_PAYER`, `AML_MANY_PAYERS` | R | Pagador compartilhado entre jogadores, ou muitos pagadores para um jogador | | `DOCUMENT_MISMATCH` | B | Chave PIX de outro titular descoberta depois do `201`: o saque vai a `rejected` | | `PLAYER_BLOCKED`, `DESTINATION_BLOCKED` | B | Lista atualizada antes do envio: o saque vai a `rejected` | | `BLOCKLIST_HIT_AT_DISPATCH` | B | Lista bateu no momento do envio de um saque `approved`: vai a `cancelled` | | `OP:` | Nenhum | Só exibição: os seus `review_reasons`, com prefixo | !!! note "Códigos fora do catálogo de regras" `BALANCE_UNAVAILABLE`, `RULES_UNAVAILABLE` e `RULES_INVALID_RESPONSE` são extensões: aparecem em `decision.reasons` quando o Dracma não consegue avaliar uma regra, e nunca liberam o saque. Os critérios que levam aos códigos de AML não são publicados. Para o depósito, os motivos de revisão são `PAYER_MISMATCH`, `PAYER_UNKNOWN`, `PAYER_BLOCKED`, `AMOUNT_MISMATCH`, `PAID_AFTER_EXPIRY`, `RULES_UNAVAILABLE` e `RULES_INVALID_RESPONSE`. Veja [Ciclo do depósito](ciclo-do-deposito.md). #### Quais limites existem Cada operação tem um conjunto de regras versionado, lido em `GET /v1/rules` (com `ETag`). Os limites abaixo existem por moeda. !!! note "Os valores são da sua operação" Os valores de partida de uma operação nova não são publicados neste guia. Eles são ajustáveis e mudam por operação. A fonte da verdade é `GET /v1/rules`. Use `GET /v1/players/{ref}/limits` para ver a posição de um jogador. | Campo | O que limita | |---|---| | `deposit_min`, `deposit_max` | Valor mínimo e máximo de um depósito | | `withdrawal_min`, `withdrawal_max` | Valor mínimo e máximo de um saque | | `auto_max_per_withdrawal` | Maior saque que sai no automático | | `auto_max_per_day` | Soma diária que o automático libera por jogador | | `max_withdrawals_per_day` | Quantos saques por dia o jogador faz no automático | | `dual_approval_above` | Valor a partir do qual a revisão exige duas pessoas | | `auto_multiplier` | O X do automático (veja abaixo) | | `windows.day`, `windows.d7`, `windows.d30` | Janelas de revisão e duras de saque | | `deposit_windows.day`, `deposit_windows.d30` | Janelas duras de depósito | | `velocity` | Quantidade de saques e de depósitos não pagos por intervalo de minutos | | `operation_auto_cap` | Teto agregado do automático da operação, por hora e por dia | | `first_withdrawal_manual` | Se o primeiro saque pago de cada jogador vai sempre para revisão | | `pix_key_change_cooldown_hours` | Carência de uma chave PIX nova | | `destination.max_distinct_30d` | Destinos distintos permitidos por jogador em 30 dias | #### Janelas: revisão e duras Cada janela de saque (dia, 7 dias, 30 dias) tem dois níveis: | Nível | Efeito | Códigos | |---|---|---| | Revisão | O saque vai a `under_review` | `WINDOW_*_AMOUNT_EXCEEDED`, `WINDOW_*_COUNT_EXCEEDED` | | Duro | `422 velocity_exceeded` | `WINDOW_HARD_LIMIT` | - O "dia" é o dia civil no fuso da operação. As janelas de 7 e 30 dias são móveis (168 e 720 horas). Veja [Dia, fuso e horários](fuso-dia-e-horarios.md). - No dia, se `auto_max_per_day` e `windows.day.review_amount` estiverem ambos definidos, vale o **menor**; o mesmo vale para a contagem (`max_withdrawals_per_day` e `windows.day.review_count`). - A velocidade (`velocity`) conta pedidos num intervalo de minutos: `VELOCITY_WITHDRAWALS` leva à revisão e `VELOCITY_HARD_LIMIT`, ao `422`. Para depósito, `DEPOSIT_VELOCITY_UNPAID` limita depósitos criados e não pagos num intervalo. Os valores são da operação. - Para depósitos, `DEPOSIT_WINDOW_EXCEEDED` aplica o limite duro das janelas `deposit_windows`. Elas vêm desligadas por padrão. #### O X do automático O automático libera um saque só se, depois dele, o total sacado não passa de **X vezes** o total depositado e liquidado: ```text (S + W) <= D * X ``` | Letra | Significa | |---|---| | `D` | Soma dos depósitos `paid` do jogador nesta moeda, liquidados pelo Dracma. Se a operação define `auto_multiplier_window_days`, só os pagos nessa janela contam | | `S` | Soma dos saques anteriores do jogador que não terminaram em `rejected`, `failed` ou `cancelled`: inclui os pendentes, os pagos e os `returned` | | `W` | O saque atual | | `X` | O `auto_multiplier` da moeda (`"0"` desliga o automático) | Exemplo com `X = 1`: o jogador depositou R$ 100,00. Um saque de R$ 80,00 passa. O seguinte, de R$ 30,00, soma R$ 110,00 e vai a revisão com `AUTO_MULTIPLIER_EXCEEDED`. Consequências para quem migra jogadores de outro sistema: - Só conta depósito **liquidado pelo Dracma**. Depósitos antigos feitos fora do Dracma não entram em `D`. - Um jogador sem depósito `paid` no Dracma recebe `NO_SETTLED_DEPOSIT` no primeiro saque: ele vai para revisão, mesmo que tenha saldo no seu sistema. - Nenhum ajuste de `D` é feito pela API. Planeje que o primeiro saque de cada jogador migrado passe por revisão, ou combine uma regra com o Dracma no onboarding. #### Teto agregado da operação `operation_auto_cap` limita quanto o automático libera **na operação inteira**, por hora e por dia (moeda a moeda). Quando um saque faria a soma passar do teto, ele vai a revisão com `OPERATION_AUTO_CAP_EXCEEDED`. O efeito é coletivo: enquanto o teto estiver estourado, **todos os jogadores** recebem revisão nos saques seguintes, até a hora ou o dia virar. Os valores estão em `GET /v1/rules`. #### Interruptor de emergência A operação pode suspender o automático inteiro. Com ele suspenso, todo saque vai a `under_review` com `OPERATION_AUTO_SUSPENDED`. O interruptor é acionado no painel ("Suspender automático") e é uma mudança que **endurece** a regra, então vale na hora. Reativar é uma mudança que afrouxa a regra e segue o fluxo de aprovação. Veja [Painel: regras e listas](../painel/regras-e-listas.md). #### Pré-checagem do seu conector O seu conector pode **endurecer** a decisão, nunca afrouxá-la. Dois campos do `POST /v1/withdrawals` fazem isso: | Campo | Efeito | |---|---| | `review_requested` | Com `true`, o saque vai **sempre** a revisão (`OPERATOR_REVIEW_REQUESTED`). | | `review_reasons` | Texto livre dos seus motivos. Aparecem no painel com o prefixo `OP:` e não viram código do Dracma. | | `player_context` | `total_deposited`, `total_withdrawn`, `account_created_at`, `net_gaming_result`. É informativo e nunca libera o automático. | Exemplo de uso: o seu sistema marca `review_requested` quando o jogador tem bônus ativo, quando há suspeita de fraude ou quando a conta é muito nova. O Dracma não conhece essas regras do seu negócio. !!! note "Bônus e regras comerciais ficam fora da API" Regras de bônus do operador não vão ao Dracma. Só o valor e os dados do jogador entram no pedido. Marque `review_requested` se quiser que uma pessoa olhe. ```json { "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-000000000001", "decision": { "mode": "manual", "reasons": ["FIRST_WITHDRAWAL", "PIX_KEY_CHANGED", "NEW_CONTEXT"], "ruleset_version": 7, "decided_by": [], "decided_at": null }, "psp_reference": null, "failure_code": null, "created_at": "2026-10-18T15:00:00Z", "updated_at": "2026-10-18T15:00:00Z", "paid_at": null } ``` ### Exemplos `GET /v1/players/{ref}/limits?currency=BRL` mostra a posição de um jogador: `total_deposited`, `total_withdrawn`, `auto_multiplier`, `auto_withdrawal_remaining` e `blocked`. `auto_withdrawal_remaining` é o quanto ainda cabe no X; outras regras (primeiro saque, chave nova, janelas, IP ausente) ainda podem mandar o saque para revisão. A forma da resposta está em [Referência: limites do jogador e saldos](../referencia/jogadores-e-saldos.md). ### Erros e casos de borda - **Motor de regras fora do ar na criação do saque.** Não é `503`: o saque é criado em `under_review` com `RULES_UNAVAILABLE`. Mantenha a reserva. No depósito, o motor fora devolve `503`. - **`player.ip` e `player.device_id` entram no risco.** O IP é **por saque** (o IP do jogador no momento do pedido, nunca o do seu servidor); o PSP de saque exige o IP do titular, e sem ele o saque vai a `under_review` com `OWNERSHIP_UNVERIFIED`. No depósito o IP é opcional. O `device_id` entra nas listas de device e no `NEW_CONTEXT`. Mande os dois sempre que souber. - **Limites de taxa por jogador.** Além das regras, a API limita pedidos por jogador (depósitos e saques por hora). Veja [Limites e quotas](../api/limites.md#limites-por-jogador). - **Mudar um limite.** A API do operador não altera regra diretamente. `POST /v1/rules/change-requests` (escopo `rules:propose`) cria um pedido que responde `202 pending_approval` e só vale depois de aprovado no painel. Mudanças que apertam a regra valem na hora; as que afrouxam esperam aprovação e um tempo de espera. Veja [Referência: regras](../referencia/regras.md#propor-post-v1ruleschange-requests) e [Painel: regras e listas](../painel/regras-e-listas.md). ### Veja também - [Ciclo do saque](ciclo-do-saque.md) - [Ciclo do depósito](ciclo-do-deposito.md) - [Dia, fuso e horários](fuso-dia-e-horarios.md) - [Saldos e caixa livre](saldos.md) - [Referência: regras](../referencia/regras.md) - [Painel: revisão de depósitos e saques](../painel/revisao-de-depositos-e-saques.md) - [Política de exceções de dinheiro](../conector/politica-de-excecoes.md) --- # Arquivo: docs/dinheiro/psps-e-ambientes.md ## PSPs > Para: Dev · Fin — Conferido em 2026-10-18. O PSP é quem move o PIX de verdade. O Dracma escolhe o PSP de cada operação e de cada direção (depósito ou saque); você não escolhe PSP por chamada. Esta página mostra quais PSPs existem, em que tipo de operação cada um roda, o que isso muda para o seu conector e o que o Dracma nunca faz. ### O que é | PSP | Depósito | Saque | Tipo de operação | |---|---|---|---| | Simulador | Sim (PIX sintético) | Sim | Só **Teste** | | MaximaxPay | Sim | Sim | Só **Real** | | Mercado Pago | Sim | **Não** | Só **Real** | Regras fixas separam os ambientes: - Uma operação **Teste** usa sempre o simulador. Nenhum PSP real é chamado e nenhum dinheiro se move. O simulador é recusado em operação Real. - Uma operação **Real** usa PSP real e move dinheiro real, em qualquer instância (hml ou prd). Os PSPs reais são recusados em operação Teste. - Na instância de produção (prd) o simulador não existe: todas as operações são Reais. - Na instância de homologação (hml), o PSP real só é usado numa operação Real que o Dracma habilitou para isso. Sem a habilitação, uma operação Real em hml não tem por onde mover dinheiro: o depósito falha e o saque fica `approved`. !!! danger "Dinheiro real" Uma operação Real move dinheiro de verdade, inclusive no hml. Combine com o Dracma, no onboarding, qual PSP a sua operação Real usa em cada instância antes de criar depósitos reais. Veja [Ambientes, tipos de operação e hosts](../conceitos/ambientes-e-hosts.md#matriz-instancia-tipo). O Mercado Pago não tem API pública de pagamento PIX para chave de terceiros. Por isso ele só atende depósitos. Uma operação que usa Mercado Pago para depositar precisa de outro PSP para sacar (hoje, a MaximaxPay). ### Quando usar Leia esta página: - no onboarding, para saber qual PSP a sua operação usa em cada direção e o que ele exige do seu conector; - ao ver muitos depósitos em `under_review` com `PAYER_UNKNOWN`; - ao ver saques parados em `approved` por falta de PSP de saque. ### Como funciona #### Quem escolhe o PSP O Dracma guarda, por operação, uma lista de conexões de PSP, cada uma com direção (`cash_in`, `cash_out` ou ambas) e prioridade. O cadastro é feito pelo time do Dracma; o operador vê o resultado em [Painel: gateways](../painel/gateways-e-psp.md). ```mermaid flowchart TD A["Novo depósito ou novo saque"] --> B{"Há conexão ativa
para a direção?"} B -->|"sim"| C["Usa a de menor prioridade
que suporta a direção"] B -->|"não"| D{"Operação Teste?"} D -->|"sim"| E["Simulador"] D -->|"não"| F["Recusado antes de sair:
depósito falha, saque fica approved"] ``` - **A escolha é fixa por tentativa.** O depósito fica no PSP escolhido na criação da cobrança. O saque usa o PSP escolhido na criação da tentativa. - **Não há failover de saque.** O Dracma nunca troca o PSP de um saque no meio do caminho nem manda o mesmo pedido a dois PSPs: isso poderia pagar duas vezes. Se o PSP de saque ficar indisponível, os saques esperam (veja [Ciclo do saque](ciclo-do-saque.md)). - **A queda de um PSP não derruba a operação.** A API do Dracma continua no ar. O depósito afetado vai a `failed` (`psp_unavailable`); o saque afetado espera. #### O que cada PSP muda para o seu conector | Tema | Mercado Pago | MaximaxPay | |---|---|---| | Campos exigidos no depósito | `player.name` e `player.email` | `player.name` | | Pagador real informado | **Não garantido.** O Dracma trata o pagador como não confirmado | O Dracma usa o pagador quando o PSP o informa | | Efeito no `paid` | Muitos depósitos vão a `under_review` com `PAYER_UNKNOWN` | Segue para `paid` quando o pagador informado confere; sem pagador informado, também vai a revisão | | Prazo da cobrança | De 31 minutos a cerca de 30 dias (veja [Ciclo do depósito](ciclo-do-deposito.md)) | Definido pelo PSP | | Saque | Não faz | Faz. Exige o IP do titular (`player.ip`) | Consequências práticas: - **Mercado Pago:** planeje equipe de revisão para os depósitos em `PAYER_UNKNOWN`, ou aceite que eles demorem. `GET /v1/currencies` avisa isso com a nota `payer_identity_unavailable` em `method_details`. - **Sempre** mande `player.name` e `player.email` do cadastro do jogador (lido do seu servidor), mesmo que a operação use MaximaxPay. Fora de produção, o e-mail precisa ser de um domínio reservado (`.test`, `.invalid`, `.example`). - **Saque com MaximaxPay:** sempre mande o IP do jogador em `player.ip`. Sem ele, o saque nunca sai no automático. - **A confirmação do pagamento** chega ao Dracma por uma notificação assinada do PSP, mas ela só dispara uma conferência: o Dracma consulta o PSP antes de levar o depósito a `paid` ou o saque a `paid`. Notificação sem assinatura válida é descartada sem efeito. - **Valide com valores mínimos.** Em operação Real, a primeira transação de depósito e de saque deve ser de valor mínimo e acompanhada dos dois lados (veja [Checklist de go-live](../go-live/checklist.md)). !!! note "Confira as exigências da sua operação" Os campos exigidos de cada operação vêm em `GET /v1/currencies`, em `method_details[].deposit_requirements`. Se a lista vier vazia e o seu PSP de depósito for o Mercado Pago, peça ao Dracma para conferir a configuração antes do go-live: sem `player.email`, a cobrança no Mercado Pago falha. Veja [Ping e moedas](../referencia/ping-e-moedas.md#method_details-e-as-exigencias-por-operacao). #### O caixa é seu, mesmo com conta compartilhada O PSP pode guardar o dinheiro de várias operações em uma mesma conta, ou dar uma conta própria à sua operação. Isso é escolha do Dracma e o operador não vê a diferença. Em qualquer dos casos, o **caixa da sua operação é separado**: `GET /v1/balances` mostra apenas a sua posição, calculada pelo livro do Dracma, e os saques da sua operação só consomem o seu caixa. Nunca existe saldo "da conta do PSP" visível na API (veja [Saldos e caixa livre](saldos.md)). #### O que você não controla - Qual PSP atende a sua operação e em que ordem. - As credenciais do PSP. Elas ficam sob guarda do Dracma e não passam pelo seu conector. - A configuração das notificações do PSP. O endereço de callback do PSP é do Dracma e é diferente do seu webhook. - O prazo e as regras próprias de cada PSP. ### Exemplos Exigências de uma operação Real que usa Mercado Pago para depósitos, como o conector as lê em `GET /v1/currencies`: ```json { "data": [ { "code": "BRL", "kind": "fiat", "exponent": 2, "methods": ["pix"], "method_details": [ { "method": "pix", "deposit_requirements": ["player.name", "player.email"], "psp_notes": ["payer_identity_unavailable"] } ] } ] } ``` ### Erros e casos de borda | Situação | O que você vê | O que fazer | |---|---|---| | Depósito: PSP indisponível | `failed` com `failure_code` `psp_unavailable` | Crie um depósito novo (`external_id` novo). Não repita o mesmo. | | Depósito: PSP recusou | `failed` com `psp_rejected` | Confira os campos exigidos da operação. | | Depósito: ambiente recusa o PSP | `failed` com `environment_guard` | Chame o suporte: a operação e o PSP não combinam (por exemplo, simulador em operação Real). | | Saque: sem PSP de saque na operação Real | O saque fica `approved` e o Dracma alerta | Chame o suporte. Veja [Ciclo do saque](ciclo-do-saque.md). | | Saque: o PSP escolhido não faz saque | `failed` com `psp_unsupported`, antes de qualquer envio | O dinheiro não saiu: devolva a reserva e chame o suporte. | | Quer usar Mercado Pago numa operação Real | O Dracma habilita por operação | Peça no onboarding. | !!! warning "Não disponível" O Dracma não oferece escolha de PSP por chamada nem failover automático de saque entre PSPs. Para trocar de PSP, peça ao Dracma. A troca vale para as próximas cobranças e tentativas, não para as que já saíram. ### Veja também - [Modelo de dinheiro](README.md) - [Ciclo do depósito](ciclo-do-deposito.md) - [Ciclo do saque](ciclo-do-saque.md) - [Tarifas](tarifas.md) - [Painel: gateways](../painel/gateways-e-psp.md) - [Ambientes, tipos de operação e hosts](../conceitos/ambientes-e-hosts.md) - [Referência: ping e moedas](../referencia/ping-e-moedas.md) --- # Arquivo: docs/dinheiro/tarifas.md ## Tarifas > Para: Fin — Conferido em 2026-10-18. O Dracma não desconta tarifa do valor que você credita no depósito nem do valor que o jogador recebe no saque. As tarifas do Dracma são combinadas em contrato e não aparecem na API. Esta página diz o que a API mostra hoje sobre tarifas e o que ela deixa de mostrar. ### O que é | Objeto | O que a API mostra sobre tarifa | |---|---| | Depósito | Nenhum campo de tarifa. O caixa da operação sobe o **valor cheio** do depósito pago. | | Saque | `fee` sempre `null` e `fees` sempre `[]`. O jogador recebe o valor cheio de `amount`. | | Relatório diário | Nenhum item `fee`. `totals.fees` sempre `"0"`. | Em números: um depósito pago de R$ 50,00 aumenta `available` em R$ 50,00. Um saque de R$ 50,00 faz o jogador receber R$ 50,00. Nenhuma tarifa é descontada de um lado nem do outro. !!! note "Reservado no contrato" O contrato da API prevê `Saque.fee`, `Saque.fees`, `Deposito.fee`, `Deposito.fees`, `kind: fee` no relatório e `totals.fees`. Hoje esses campos nunca são preenchidos: `fee` vem `null`, `fees` vem `[]` e `totals.fees` vem `"0"`. Trate a ausência como "sem tarifa visível na API", nunca como "tarifa zero no seu contrato". ### Quando usar Leia esta página ao: - montar o fluxo de caixa e o orçamento do financeiro; - escrever a conferência diária (os totais não trazem tarifa); - perguntar por que a API não tem extrato de tarifas. ### Como funciona #### Tarifa do Dracma A tarifa comercial do Dracma é definida em **contrato**, caso a caso. Este guia não publica valores nem modelos de cobrança. A API não calcula, não desconta e não mostra tarifa do Dracma: o crédito do depósito e o valor do saque são sempre integrais. !!! warning "Não disponível" O Dracma não oferece extrato de tarifas, de uso ou de faturamento pela API nem pelo painel. Se você precisa de um demonstrativo, peça pelo canal de suporte combinado no onboarding. O que a API oferece para conferência é o relatório diário (veja [Reconciliar com o seu livro](reconciliacao-do-operador.md)), que não traz tarifa. #### Tarifa do PSP Cada PSP tem a sua própria tarifa. O Dracma não a repassa no crédito do depósito nem no valor do saque, e **nenhum item `fee` aparece no relatório diário**. O guia não publica valores, tetos nem a forma como o Dracma acerta a tarifa com os PSPs: isso faz parte do contrato comercial. Consequências para a sua conferência: - `totals.fees` é `"0"` e o relatório não tem item `fee`. - O caixa da operação só se move pelos movimentos listados em [Modelo de dinheiro](README.md). - Se `available` ou o caixa livre mudar por um motivo que esses movimentos não explicam, abra um chamado com a data, a moeda e os `trace_id` envolvidos (veja [Suporte](../ajuda/suporte.md)). Não ajuste o seu livro por conta própria. #### Uso e consumo O painel não tem tela de uso, consumo ou faturamento. O que existe de volume é o relatório diário e o extrato de transações (veja [Painel: transações](../painel/transacoes.md)). Para totais por período, some os itens `deposit` e `withdrawal` do relatório de cada dia. ### Exemplos Trecho de um saque: `fee` nulo, `fees` vazio e o jogador recebendo o valor cheio de `amount`. ```json { "id": "wdr_0192a3b40c6d7e8f9a0b1c2d3e4f5a6c", "external_id": "wdr_0192a3b4-5c6d-7e8f-9a0b-1c2d3e4f5a6c", "status": "paid", "version": 5, "amount": { "amount": "5000", "currency": "BRL" }, "fee": null, "fees": [], "method": "pix", "player_ref": "0192a3b4-0000-7000-8000-000000000001", "decision": { "mode": "automatic", "reasons": [], "ruleset_version": 7, "decided_by": [], "decided_at": "2026-10-18T15:00:01Z" }, "psp_reference": "E00000000202610181500AbCdEfGhIjKl", "failure_code": null, "created_at": "2026-10-18T15:00:00Z", "updated_at": "2026-10-18T15:00:09Z", "paid_at": "2026-10-18T15:00:09Z" } ``` ### Erros e casos de borda - **`kind: fee` num cliente tolerante.** Se o relatório trouxer um item `fee`, ele entra no digest como qualquer outro. O seu conector deve tratar `kind` desconhecido como item de revisão, sem lançamento automático no livro (veja [Reconciliar com o seu livro](reconciliacao-do-operador.md)). - **Tarifa e `Money`.** Qualquer tarifa visível na API é `Money` (inteiro na menor unidade) em `fee`, nunca float. - **Não confunda `fee: null` com "sem tarifa no contrato".** A API só diz que não há tarifa visível nela. ### Veja também - [Modelo de dinheiro](README.md) - [Reconciliar com o seu livro](reconciliacao-do-operador.md) - [PSPs](psps-e-ambientes.md) - [Referência: reconciliação](../referencia/reconciliacao.md) - [Referência: superfícies reservadas](../referencia/superficies-reservadas.md) --- # Arquivo: docs/dinheiro/devolucoes-estornos-e-disputas.md ## Devoluções, estornos e disputas > Para: Fin · Dev — Conferido em 2026-10-18. Esta página separa o que existe de verdade em produção do que só existe no sandbox: devolução de depósito, devolução de saque pelo banco, estorno, chargeback e disputa. Cada caso tem um efeito diferente no caixa da operação e no seu livro. Construa o seu conector para todos eles, mas não espere os eventos de estorno e chargeback em produção. ### O que é | Caso | Estado | Produção | Sandbox | |---|---|---|---| | Depósito devolvido pelo revisor | `deposit` em `returned` | Existe | Existe | | Saque devolvido pelo banco depois de pago | `withdrawal` em `returned` | O mecanismo existe. O guia não afirma quais PSPs informam devolução hoje | Existe, por ação de sandbox | | Estorno de depósito pago | `deposit` em `refunded` | **Sem gatilho** | Existe | | Chargeback ou MED de depósito pago | `deposit` em `chargeback` | **Sem gatilho** | Existe | | Disputa de cartão | Nenhum | Não disponível | Não disponível | | Recall de saque | `recalled` | Não ocorre em PIX | Não ocorre em PIX | "Sem gatilho" quer dizer: o estado e o tratamento no caixa existem no Dracma, mas nenhum PSP nem rotina da produção leva um depósito até lá hoje. Teste o seu tratamento no sandbox. ### Quando usar Leia esta página ao: - escrever o tratamento de `refunded`, `chargeback` e `returned` no seu conector; - explicar ao financeiro por que o caixa mudou depois de um depósito que já estava `paid`; - decidir a política de débito do jogador (veja [Política de exceções de dinheiro](../conector/politica-de-excecoes.md)). ### Como funciona #### Efeito de cada caso | Caso | Caixa da operação (`available`) | Item no relatório diário | O que fazer no seu livro | |---|---|---|---| | Depósito `returned` | Não muda (nunca foi creditado) | `return`, pelo valor pago | Nada | | Saque `returned` | Sobe o valor **devolvido** (pode ser parcial) | `return`, pelo valor devolvido | Política sua; recomendação: abrir caso, não recreditar sozinho | | Depósito `refunded` | Cai o valor do depósito | `refund` | Débito conforme a sua política; recomendação: abrir caso | | Depósito `chargeback` | Cai o valor do depósito | `chargeback` | Idem | Pontos que costumam surpreender: - **Estorno e chargeback debitam o caixa pelo valor cheio.** Não há tarifa no movimento (veja [Tarifas](tarifas.md)). - **O caixa livre pode ficar negativo.** Se o jogador já sacou e há outros saques reservados, o débito do estorno ou do chargeback pode deixar o caixa livre abaixo de zero. Enquanto isso durar, novos saques recebem `422 insufficient_operation_balance` (veja [Saldos e caixa livre](saldos.md)). - **Depósito `returned` não debita.** Ele nunca passou por `paid`, então o relatório mostra só o item `return` e nenhum item `deposit` para ele. - **Saque devolvido continua com o item `withdrawal` do dia do pagamento.** O item `return` aparece no dia da devolução; não há compensação automática dentro do total `withdrawals_paid` (veja [Reconciliar com o seu livro](reconciliacao-do-operador.md)). #### Por que não mover dinheiro sozinho Um débito automático num jogador que já sacou deixa saldo negativo. Um recrédito automático de um saque devolvido pode pagar duas vezes se o banco reenviar. Esses casos são raros e caros: a recomendação é abrir um caso com alerta e deixar uma pessoa decidir. O modelo está em [Política de exceções de dinheiro](../conector/politica-de-excecoes.md). #### Disputas de cartão !!! warning "Não disponível" O Dracma não oferece fluxo de disputa para o operador. Não há API, painel ou evento para abrir, acompanhar ou contestar uma disputa, e não há tratamento de chargeback de cartão: o único meio de pagamento é o PIX. Chargeback e MED de depósito não são avisados por evento em produção. Se o seu banco ou o PSP comunicarem uma disputa, trate-a fora da API e informe o suporte. #### Recall `POST /v1/withdrawals/{id}/recall` só vale para saques por transferência bancária, que o Dracma não oferece. Num saque PIX a rota responde `409 invalid_state_transition` sem mudar nada. Os estados `recall_requested` e `recalled` e o item `recall` do relatório são reservados. ### Exemplos No sandbox, o estorno e o chargeback saem de um depósito `paid`: ```text POST /v1/sandbox/deposits/{id}/refund -> 200, deposit.refunded POST /v1/sandbox/deposits/{id}/chargeback -> 200, deposit.chargeback ``` Cada ação exige `Idempotency-Key` e só vale a partir de `paid`; em qualquer outro estado a resposta é `409`. As rotas existem apenas em operação Teste. Use-as para provar que o seu conector não credita nem debita sozinho ao ver `refunded` e `chargeback` (receita no [Sandbox: receitas](../sandbox/receitas.md#receita-12-estorno-e-chargeback-depois-de-paid)). Para o saque, a ação `return` de sandbox leva um saque `paid` a `returned`, com o objeto `return` preenchido (veja [Ciclo do saque](ciclo-do-saque.md)). ### Erros e casos de borda - **Estado desconhecido.** Trate como não terminal. Não mova dinheiro. - **Chargeback depois de saque.** O jogador já sacou: o caixa pode ficar negativo e o saldo do jogador no seu livro, também. Registre o caso e decida com uma pessoa. - **Devolução duplicada.** Um segundo `withdrawal.returned` para o mesmo saque não credita o caixa de novo. O seu conector também deve ser idempotente pelo `external_id`. - **Devolução inválida.** Valor zero, maior que o pago ou em outra moeda é recusado pelo Dracma e gera alerta; o saque não muda. ### Veja também - [Modelo de dinheiro](README.md) - [Ciclo do depósito](ciclo-do-deposito.md) - [Ciclo do saque](ciclo-do-saque.md) - [Política de exceções de dinheiro](../conector/politica-de-excecoes.md) - [Reconciliar com o seu livro](reconciliacao-do-operador.md) - [Sandbox: receitas](../sandbox/receitas.md) --- # Arquivo: docs/dinheiro/reconciliacao-do-operador.md ## Reconciliar com o seu livro > Para: Fin · Dev — Conferido em 2026-10-18. Todo dia, o Dracma entrega um relatório dos movimentos liquidados, com um digest assinado. A sua reconciliação compara esse relatório com o seu livro. Esta página mostra o que cada tipo de item significa para o seu livro, o que o relatório não traz e como investigar uma diferença. Parâmetros, digest e assinatura estão em [Referência: reconciliação](../referencia/reconciliacao.md). ### O que é O relatório (`GET /v1/reconciliation`) lista, por dia UTC e por moeda, cada movimento que se tornou definitivo no dia. Os tipos (`kind`) que o Dracma emite hoje são: | `kind` | O que representa | `status` do item | |---|---|---| | `deposit` | Depósito que chegou a `paid` | `paid` | | `withdrawal` | Saque que chegou a `paid` | `paid` | | `refund` | Estorno de depósito | `refunded` | | `chargeback` | Chargeback ou MED de depósito | `chargeback` | | `return` | Depósito devolvido em revisão, ou saque devolvido pelo banco | `returned` | !!! note "Reservado no contrato" Os tipos `fee`, `recall` e `receipt` fazem parte do contrato, mas o Dracma não emite nenhum deles hoje. `fee` aparece como item só se houver tarifa visível (hoje não há; veja [Tarifas](tarifas.md)). `recall` e `receipt` pertencem a transferências bancárias, que o Dracma não oferece. Se algum desses tipos chegar, trate como item de revisão, sem lançamento automático no seu livro. ### Quando usar Use esta página ao montar a rotina diária de conferência e ao investigar uma diferença entre o seu livro e o relatório. Rotina recomendada: | Quando | O quê | |---|---| | A cada 5 minutos | `GET /v1/events?after=<último evento>`: deduplique, faça o fetch-back e aplique o efeito. Veja [Webhooks: recuperação](../webhooks/recuperacao-get-events.md). | | Diariamente, depois das 02:00 UTC | `GET /v1/reconciliation` do dia anterior, para cada moeda: confira o digest e a assinatura e compare com o livro. | | Continuamente | `GET /v1/balances`: alerte quando o caixa livre ficar baixo (veja [Saldos e caixa livre](saldos.md)). | ### Como funciona #### O que cada item significa para o seu livro | `kind` | Valor do item (`amount`) | `settled_at` | Sentido no seu livro | |---|---|---|---| | `deposit` | Valor pago (igual ao do depósito) | Instante do pagamento (`paid_at`) | Um crédito do jogador, com o mesmo `external_id` | | `withdrawal` | Valor do saque | Instante do pagamento (`paid_at`) | Uma reserva finalizada | | `refund` | Valor do depósito | Instante do evento de estorno | Um débito, conforme a sua política | | `chargeback` | Valor do depósito | Instante do evento | Um débito, conforme a sua política | | `return` (depósito) | `paid_amount` do depósito | Instante da devolução | Nenhum lançamento: o depósito nunca foi creditado | | `return` (saque) | `return.returned_amount`, que pode ser **parcial** | Instante da devolução | Política sua: o caixa da operação recebeu o valor devolvido | #### Depósito devolvido: só o item `return` Um depósito `returned` nunca passou por `paid`, então **nunca houve crédito**. O relatório mostra apenas o item `return` desse depósito e **nenhum item `deposit`**. Se o seu livro creditou o jogador antes da devolução, você tem um crédito sem par. #### Saque devolvido: dois itens em dias diferentes Um saque devolvido pelo banco aparece duas vezes: 1. Como `withdrawal` com `status: "paid"`, no dia do pagamento. 2. Como `return` com `status: "returned"`, no dia da devolução, pelo valor devolvido. O Dracma **não compensa** o item `withdrawal` quando há devolução: o total `withdrawals_paid` continua contando o saque pago. A devolução é um segundo item, em outro dia. #### O total de devoluções não existe `totals` traz `deposits_paid`, `withdrawals_paid`, `refunds`, `chargebacks`, `fees` e `count`. **Não há `returns`**. Os itens `return` contam em `count`, mas não entram em nenhum total de valor. !!! warning "Não disponível" O relatório não traz o total de devoluções. Por isso, a regra "os totais batem com a soma dos itens" só vale para os tipos que têm total. Some os itens `return` à parte, no seu lado, e confira essa soma com o que o seu livro lançou. O relatório também não tem item de ajuste ou de correção. Conferência dos totais, tipo a tipo: | Total | Como conferir | |---|---| | `deposits_paid` | Soma dos itens `deposit` | | `withdrawals_paid` | Soma dos itens `withdrawal` | | `refunds` | Soma dos itens `refund` | | `chargebacks` | Soma dos itens `chargeback` | | `fees` | Sempre `"0"` (sem itens `fee`) | | `count` | Todos os itens, inclusive `return` | | Devoluções | Soma dos itens `return`, fora dos totais | #### O que comparar com o seu livro Para cada dia e cada moeda: 1. Todo `deposit` do relatório tem **exatamente um** crédito no seu livro, com o mesmo `external_id` e o mesmo valor. 2. Todo crédito seu tem um `deposit` no relatório. Um crédito sem par é o caso mais grave: é dinheiro criado sem liquidação. 3. Todo `withdrawal` corresponde a uma reserva sua finalizada, e vice-versa. 4. `refund` e `chargeback` correspondem às decisões que você registrou. 5. Todo `return` corresponde a um caso aberto (devolução de depósito ou de saque). 6. Os totais de depósito, saque, estorno e chargeback batem com a soma dos itens do tipo. **Divergência é incidente.** Não ajuste o seu livro para fazê-lo bater. Abra o incidente, congele o que for preciso, investigue e registre. Dispare alerta quando o digest ou a assinatura não baterem, ou quando faltar um par. #### Rotina diária (pseudocódigo) O horário do job é seu. Escolha um horário depois das 02:00 UTC (por exemplo, 02:30 UTC) e rode uma vez por moeda habilitada: ```text para cada moeda em GET /v1/currencies: itens = baixar todas as páginas de GET /v1/reconciliation?date=D-1¤cy=moeda se 409 day_open: reagendar para depois das 02:00 UTC, sem laço conferir digest e assinatura # veja a referência para cada item em itens: casar com o seu livro por (external_id, kind) para cada lançamento do seu livro no dia, sem item correspondente: abrir incidente "crédito sem par" somar os itens "return" à parte e conferir publicar métricas: itens, sem par, total por tipo, duração da rotina ``` Métricas úteis para o seu painel: quantidade de itens sem par, diferença de valor por tipo, idade do último dia reconciliado e duração da rotina. #### O que a reconciliação não prova !!! warning "Não disponível" O relatório prova o livro do Dracma, não o banco. O Dracma não importa nem guarda o extrato do PSP, e o painel não faz a conferência automática com ele. Se você quer provar que o dinheiro chegou à conta bancária, use o extrato do próprio PSP ou do seu banco. O painel de reconciliação (veja [Painel: reconciliação](../painel/reconciliacao.md#conferir-com-o-seu-livro)) exporta o mesmo relatório e permite importar o seu livro para comparar. ### Exemplos Relatório de 2026-10-19 com duas devoluções: um depósito devolvido em revisão (sem item `deposit`) e um saque pago no dia anterior que o banco devolveu **parcialmente** (R$ 30,00 de R$ 50,00). Note que `count` é 2 e todos os totais de valor são zero, porque `return` não tem total. ```json { "date": "2026-10-19", "currency": "BRL", "items": [ { "object_id": "dep_0192a3b40c6d7e8f9a0b1c2d3e4f5a71", "external_id": "dep_0192a3b4-5c6d-7e8f-9a0b-1c2d3e4f5a71", "kind": "return", "status": "returned", "amount": { "amount": "5000", "currency": "BRL" }, "fee": null, "settled_at": "2026-10-19T11:20:00Z" }, { "object_id": "wdr_0192a3b40c6d7e8f9a0b1c2d3e4f5a6c", "external_id": "wdr_0192a3b4-5c6d-7e8f-9a0b-1c2d3e4f5a6c", "kind": "return", "status": "returned", "amount": { "amount": "3000", "currency": "BRL" }, "fee": null, "settled_at": "2026-10-19T10:00:00Z" } ], "next_cursor": null, "totals": { "deposits_paid": { "amount": "0", "currency": "BRL" }, "withdrawals_paid": { "amount": "0", "currency": "BRL" }, "refunds": { "amount": "0", "currency": "BRL" }, "chargebacks": { "amount": "0", "currency": "BRL" }, "fees": { "amount": "0", "currency": "BRL" }, "count": 2 }, "digest": "8385cc0772eb313749c1114c88e5ba6889b60f8d10ad5c69f99f364a891c52c6" } ``` Os itens estão em ordem de `object_id`, como o digest exige. O mesmo saque `wdr_0192a3b40c6d7e8f9a0b1c2d3e4f5a6c` aparece também como `withdrawal` no relatório do dia 18. ### Erros e casos de borda #### Investigar uma diferença 1. Anote o `object_id`, o `external_id` e o `kind` do item. 2. Leia o objeto: `GET /v1/deposits/{id}` ou `GET /v1/withdrawals/{id}`. Confira `status`, `version`, `paid_amount` e, no saque devolvido, `return`. 3. Liste a história do objeto com `GET /v1/events?object_id=`. Veja em que ordem os estados chegaram e quando. 4. Compare com o seu registro: o seu livro reagiu a todos os eventos? Houve evento duplicado ou perdido? 5. Para a investigação com o Dracma, guarde o `trace_id` da resposta e o `Dracma-Event-Id` do webhook (veja [Suporte](../ajuda/suporte.md)). #### Casos comuns | Sintoma | Causa provável | O que fazer | |---|---|---| | Crédito seu sem `deposit` no relatório | O depósito foi `returned`, ou você creditou antes de `paid` | Estorne o seu crédito conforme a política e abra caso | | `deposit` sem crédito seu | Webhook perdido ou fetch-back não executado | Recupere por `GET /v1/events`; credite uma vez, pelo `external_id` | | Item `return` que você não esperava | Depósito devolvido em revisão ou saque devolvido pelo banco | Veja o objeto e abra caso | | Total de valor não bate com os itens | Item `return` fora dos totais | Some os itens `return` à parte | | Item de dia vizinho | Fuso: você comparou pela data local | Compare pelo `settled_at` em UTC | | `409 day_open` | O dia ainda não abriu (antes de 02:00 UTC de `D+1`) | Aguarde, sem laço | | `503` na última página | O Dracma não conseguiu assinar ou recalcular o digest | Repita depois do `Retry-After`; se persistir, chame o suporte | ### Veja também - [Referência: reconciliação](../referencia/reconciliacao.md) - [Modelo de dinheiro](README.md) - [Devoluções, estornos e disputas](devolucoes-estornos-e-disputas.md) - [Dia, fuso e horários](fuso-dia-e-horarios.md) - [Runbook de dinheiro](runbook.md) - [Painel: reconciliação](../painel/reconciliacao.md) --- # Arquivo: docs/dinheiro/runbook.md ## Runbook de dinheiro > Para: Fin · Dev — Conferido em 2026-10-18. Este runbook leva do sintoma ao diagnóstico e à ação, com a verificação que prova que o problema acabou e o ponto em que você chama o suporte. Use-o quando um saldo, um saque, um depósito ou o relatório diário não fizer o que você espera. ### O que é Cada entrada tem cinco passos: **sintoma** (o que você viu), **diagnóstico** (o que consultar), **ação** (o que fazer), **verificação** (como saber que resolveu) e **escalar** (quando chamar o suporte). Os termos estão em [Modelo de dinheiro](README.md). Regras que valem em toda entrada: 1. Não devolva a reserva de um saque sem resposta conclusiva (`rejected`, `failed`, `cancelled` ou `422` síncrono). 2. Não reenvie um saque com `external_id` novo para "tentar de novo" enquanto o anterior não terminou. 3. Não ajuste o seu livro para bater com o relatório. Abra incidente. 4. Sempre consulte o objeto pela API antes de agir. O webhook sozinho não autoriza dinheiro. ### Quando usar | Se você viu | Vá para | |---|---| | Saque em `processing` há muito tempo | Saque preso em `processing` | | Saque em `approved` que não sai | Saque `approved` parado | | `422 insufficient_operation_balance` | Sem caixa livre | | Digest, assinatura ou total que não bate | Relatório que não bate | | Item `return` inesperado | Item `return` inesperado | | Caixa livre negativo | Caixa livre negativo | | Depósito em `under_review` há horas | Depósito em revisão | | `paid_amount` num depósito `expired` | Pagamento tardio | | Faixa "Saques em verificação" | Saques em verificação | | `GET /v1/balances` com `503` | Saldo indisponível | ### Como funciona #### Saque preso em `processing` | Passo | O que fazer | |---|---| | Sintoma | `status` continua `processing` por mais tempo que o normal | | Diagnóstico | `GET /v1/withdrawals/{id}`: confira `status`, `version`, `updated_at`. `GET /v1/events?object_id={id}` mostra a história | | Ação | Se `updated_at` tem menos de cerca de 1 h 30: espere, o Dracma consulta o PSP sozinho. **Mantenha a reserva.** Não reenvie, não cancele (responde `409`) e não devolva | | Verificação | O saque chega a `paid` ou `failed`. Só em `failed` a reserva volta ao jogador | | Escalar | Se passar de 2 horas em `processing`: chamado com `id` do saque, `external_id` e `trace_id`. O Dracma trata o caso manualmente; nunca reenvia nem troca de PSP | #### Saque `approved` parado | Passo | O que fazer | |---|---| | Sintoma | `status: "approved"` e `psp_reference` nulo, por mais de alguns minutos | | Diagnóstico | Verifique, nesta ordem: (1) fora do horário do automático? ([Dia, fuso e horários](fuso-dia-e-horarios.md)); (2) caixa livre menor que o valor? ([Saldos e caixa livre](saldos.md)); (3) o painel mostra a faixa "Saques em verificação"?; (4) o saque tem `player.ip`? | | Ação | (1) e (2): espere o horário abrir ou o caixa crescer; o Dracma reconfere e envia sozinho. (3): aguarde. (4): sem IP o saque não sai; cancele e peça de novo com o IP do jogador. Em qualquer caso, você pode cancelar com `POST /v1/withdrawals/{id}/cancel` enquanto o envio não saiu | | Verificação | O saque passa a `processing`, ou a `cancelled`. Se `cancelled`, devolva a reserva | | Escalar | Se nenhuma das quatro causas explica: chamado com `id` e `trace_id`. Um envio recusado antes de sair deixa o saque `approved` e **não é reenviado sozinho** | #### Sem caixa livre | Passo | O que fazer | |---|---| | Sintoma | `422 insufficient_operation_balance` na criação do saque | | Diagnóstico | `GET /v1/balances`: calcule `available - pending_out` | | Ação | Devolva a reserva do jogador (o `422` não cria saque). Para tentar de novo, use `external_id` e `Idempotency-Key` novos, depois que houver caixa livre | | Verificação | Caixa livre maior ou igual ao valor do saque | | Escalar | Se o caixa livre parece certo e o `422` continua: chamado com a leitura de `GET /v1/balances` e o `trace_id` | #### Relatório que não bate | Passo | O que fazer | |---|---| | Sintoma | O digest que você calculou é diferente do digest da última página, ou a assinatura não confere, ou um total não bate com a soma dos itens | | Diagnóstico | (1) Você juntou **todas** as páginas? (2) Ordenou por `(object_id, kind)`? (3) Usou `settled_at` e `amount` exatamente como vieram? (4) O total que não bate é de devoluções? Elas não têm total ([Reconciliar com o seu livro](reconciliacao-do-operador.md)) | | Ação | Corrija o seu cálculo se a causa for (1) a (4). Se o digest ou a assinatura continuarem errados, trate como incidente: congele a conferência do dia e não ajuste o livro | | Verificação | O digest recalculado é igual ao da última página, e a assinatura confere | | Escalar | Digest ou assinatura errados com o cálculo correto: chamado com a data, a moeda, o digest recebido e o `Dracma-Signature` | #### Item `return` inesperado | Passo | O que fazer | |---|---| | Sintoma | O relatório tem um item `return` sem caso aberto do seu lado | | Diagnóstico | Veja o objeto pelo `object_id`. Se for `dep_`: o depósito foi devolvido em revisão. Se for `wdr_`: o banco devolveu o PIX; veja `return.returned_amount` | | Ação | Depósito: nada no livro (nunca foi creditado); registre. Saque: abra caso em `needs_decision` ([Política de exceções](../conector/politica-de-excecoes.md)); o caixa da operação já recebeu o valor devolvido | | Verificação | O caso está aberto e o item `return` conferido | | Escalar | Devolução em valor diferente do esperado: chamado com o `id` do saque | #### Caixa livre negativo | Passo | O que fazer | |---|---| | Sintoma | `available - pending_out` abaixo de zero | | Diagnóstico | Houve estorno ou chargeback de depósito (hoje só no sandbox)? Confira os itens `refund` e `chargeback` do dia | | Ação | Novos saques recebem `422` até o caixa se recuperar. Segure os saques no seu lado e avise o financeiro. Depósitos pagos novos recompõem o caixa | | Verificação | `available - pending_out` volta a ser positivo | | Escalar | Caixa negativo em produção: chamado imediato com a leitura de `GET /v1/balances` | #### Depósito em revisão | Passo | O que fazer | |---|---| | Sintoma | `status: "under_review"` por horas | | Diagnóstico | Leia `review_reasons`. Com Mercado Pago, `PAYER_UNKNOWN` é esperado e demora mais | | Ação | Quem decide é o revisor no painel ([Painel: revisão](../painel/revisao-de-depositos-e-saques.md#decidir-um-item)). Não credite o jogador. Diga ao jogador apenas "pagamento em análise" | | Verificação | O depósito vai a `paid` (credite uma vez) ou `returned` (nada a creditar) | | Escalar | Se a revisão não andar no prazo da sua operação: chamado com o `id` do depósito | #### Pagamento tardio | Passo | O que fazer | |---|---| | Sintoma | Depósito `expired`, `cancelled` ou `failed` com `paid_amount` e `paid_at` preenchidos | | Diagnóstico | O jogador pagou depois de o depósito terminar. O Dracma gravou o pagamento sem mudar o estado | | Ação | **Não credite.** Abra caso em `needs_decision` ([Política de exceções](../conector/politica-de-excecoes.md)) e chame o suporte para decidir o destino do valor | | Verificação | O caso tem desfecho registrado | | Escalar | Sempre: informe o suporte, com o `id` do depósito, para decidir o destino do valor recebido | #### Saques em verificação | Passo | O que fazer | |---|---| | Sintoma | A faixa "Saques em verificação" no painel; saques novos ficam em `approved` | | Diagnóstico | A faixa é neutra de propósito e não traz motivo. Os depósitos seguem normalmente | | Ação | Aguarde. Mantenha as reservas. Avise o suporte do jogador de que os saques podem demorar mais que o normal | | Verificação | A faixa some e os saques passam a `processing` | | Escalar | Se a faixa durar mais do que o seu prazo interno: chamado pelo canal combinado no onboarding. O Dracma não publica prazo | #### Saldo indisponível | Passo | O que fazer | |---|---| | Sintoma | `GET /v1/balances` responde `503` | | Diagnóstico | O livro do Dracma está fora do ar. Novos saques entram em `under_review` com `BALANCE_UNAVAILABLE` | | Ação | Repita depois do `Retry-After`. Não decida saque com o último valor lido | | Verificação | `200` com `as_of` recente | | Escalar | `503` por mais tempo que a sua tolerância: chamado | ### Exemplos O que enviar ao suporte, em qualquer entrada: ```text Operação: Objeto: wdr_0192a3b40c6d7e8f9a0b1c2d3e4f5a6c (external_id wdr_0192a3b4-5c6d-7e8f-9a0b-1c2d3e4f5a6c) Estado lido: processing (version 3, updated_at 2026-10-18T15:00:03Z) trace_id: 4bf92f3577b34da6a3ce929d0e0e4736 O que você já fez: consultou GET /v1/events?object_id=..., manteve a reserva ``` ### Erros e casos de borda - **Pressa não autoriza mexer em dinheiro.** Nenhuma entrada deste runbook pede reenviar, trocar de PSP ou recreditar sem desfecho conclusivo. - **Erro de rede não é resposta.** Timeout, `5xx` e `429` pedem a mesma `Idempotency-Key` ou uma consulta por `external_id`, nunca um pedido novo. - **Vários sintomas ao mesmo tempo.** Resolva primeiro o caixa livre (afeta todos os saques) e depois os objetos. ### Veja também - [Modelo de dinheiro](README.md) - [Saldos e caixa livre](saldos.md) - [Ciclo do saque](ciclo-do-saque.md) - [Ciclo do depósito](ciclo-do-deposito.md) - [Reconciliar com o seu livro](reconciliacao-do-operador.md) - [Suporte](../ajuda/suporte.md) - [Alertas do conector](../conector/alertas-e-observabilidade.md) --- # Arquivo: docs/conector/estados-no-produto.md ## Estados no seu produto > Para: Dev — Conferido em 2026-10-18. O jogador não deve ver os estados do Dracma. Ele vê um conjunto menor e estável de estados do seu produto, que o seu conector calcula a partir do último estado do Dracma **aplicado**. Esta página dá a tabela de tradução (Dracma, seu produto, efeito no seu livro, mensagem) e a regra para calcular o estado sem corrida. ### O que é A regra geral: **o estado mostrado ao jogador nunca adianta dinheiro.** `paid` só aparece depois de o crédito do depósito (ou a finalização do saque) estar gravado no seu livro. Conjunto sugerido de estados do seu produto: | Fluxo | Estados do produto | |---|---| | Depósito | `creating`, `pending`, `paid`, `expired`, `cancelled`, `failed`, `under_review` | | Saque | `processing`, `under_review`, `paid`, `rejected`, `failed`, `cancelled` | Dois itens ajudam a calcular o estado: - O estado local do seu registro de envio (por exemplo, uma fila de saída com o pedido ao Dracma). - O último estado do Dracma **aplicado**, lido por fetch-back (`GET /v1/deposits/{id}` ou `GET /v1/withdrawals/{id}`). ### Quando usar Use a tabela ao desenhar as telas e o contrato do seu front com o seu back. Troque os nomes dos estados do produto pelos seus; o que importa é a coluna "efeito no seu livro". ### Como funciona #### Depósito: Dracma para o seu produto | Situação | Estado do Dracma (fetch-back) | Seu produto mostra | Efeito no seu livro | Mensagem sugerida | |---|---|---|---|---| | Pedido gravado, chamada sem resposta conclusiva (timeout, `5xx`, `429`) | Nenhum | `creating` | Nenhum | "Gerando seu PIX..." | | `202` do Dracma | `created` | `creating` | Nenhum | "Gerando seu PIX..." | | PIX gerado | `pending` | `pending`, com o copia-e-cola | Nenhum | "Pague o PIX até {hora}." | | Pagamento em análise | `under_review` | `under_review` | Nenhum | "Pagamento recebido, em análise." | | Crédito gravado | `paid` | `paid` | **Credita uma vez** | "Depósito confirmado." | | Prazo vencido | `expired` | `expired` | Nenhum | "O PIX expirou. Gere outro." | | Cancelado | `cancelled` | `cancelled` | Nenhum | "Depósito cancelado." | | Não foi possível gerar o PIX | `failed` | `failed` | Nenhum | "Não foi possível gerar o PIX. Tente de novo." | | Devolvido em revisão | `returned` | `failed` | Nenhum | "O pagamento não pôde ser aceito. Fale com o suporte." | | Já creditado, depois estorno ou chargeback | `refunded` ou `chargeback` | `paid` (continua) | **Não mexe**: abre caso | Nenhuma ao jogador; o caso é tratado internamente | | Estado do contrato que o PIX não usa | `pending_compliance`, `frozen`, `paid_partial` | `under_review` | Nenhum | "Pagamento em análise." | | Estado desconhecido | Qualquer outro | Mantém o último estado mostrado (ou `under_review` se era `pending`) | Nenhum | "Pagamento em análise." | | Recusa `4xx` na criação | Sem objeto | Erro para o front | Nenhum | Mensagem genérica de erro | | Dracma indisponível ou não configurado | Sem objeto | Erro `503` do seu produto | Nenhum | "Depósito indisponível no momento." | #### Saque: Dracma para o seu produto | Situação | Estado do Dracma (fetch-back) | Seu produto mostra | Reserva no seu livro | Mensagem sugerida | |---|---|---|---|---| | Pedido e reserva gravados, sem resposta conclusiva | Nenhum | `processing` | **Mantida** | "Saque em processamento." | | Chave PIX em consulta | `requested` | `processing` | Mantida | "Saque em processamento." | | Aprovado ou em envio | `approved`, `processing` | `processing` | Mantida | "Saque em processamento." | | Em revisão | `under_review` | `under_review` | Mantida | "Saque em análise." | | Finalização gravada | `paid` | `paid` | **Finalizada** (saída definitiva) | "Saque enviado." | | Recusado | `rejected` | `rejected` | **Devolvida** | "Saque não aprovado. O valor voltou para sua carteira." | | Falhou | `failed` | `failed` | **Devolvida** | "Não foi possível enviar o PIX. O valor voltou para sua carteira." | | Cancelado | `cancelled` | `cancelled` | **Devolvida** | "Saque cancelado. O valor voltou para sua carteira." | | Já finalizado, banco devolveu | `returned` | `under_review` | **Não mexe**: abre caso | "Saque em análise." | | Estados do contrato que o PIX não usa | `pending_compliance`, `sent`, `in_transit`, `recall_requested` | `processing` | Mantida | "Saque em processamento." (nunca diga "compliance") | | Estado reservado | `recalled` | `under_review` | Não mexe: abre caso | "Saque em análise." | | Estado desconhecido | Qualquer outro | `processing` | Mantida | "Saque em processamento." | | Recusa `4xx` na criação (`422`, `400`) | Sem objeto | Erro de negócio no seu produto | **Devolvida** | Mensagem do seu produto | | `409 idempotency_key_reused` no envio | Sem objeto | `processing` | Mantida: abre caso | "Saque em processamento." | #### Mensagens ao jogador - **Nunca explique `under_review` com o motivo** (`PAYER_MISMATCH`, `AMOUNT_MISMATCH`, `FIRST_WITHDRAWAL`...). O suporte vê o motivo; o jogador, não. - **Devolução de depósito.** `returned` não existe na lista de estados do produto: mostre `failed` com a mensagem de pagamento não aceito. Evite afirmar como o dinheiro volta ao pagador; encaminhe ao suporte. - **Devolução de saque.** `returned` aparece depois de o jogador ter visto `paid`: mostre "Saque em análise" e abra um caso. Se o seu suporte recreditar o jogador, peça a ele para cadastrar outra chave. - **Nunca diga "compliance", "sanção" ou "lista"** ao jogador, mesmo que o estado do contrato os mencione. #### Como calcular o estado sem corrida ```text estado_do_produto(registro): se registro.versao_dracma é nula: # o Dracma ainda não respondeu de forma conclusiva devolva "creating" (depósito) ou "processing" (saque) s = registro.estado_dracma # último estado APLICADO, por fetch-back devolva TABELA[s] ou PADRAO_DESCONHECIDO ``` - Aplique um estado novo **só se a `version` for maior** que `registro.versao_dracma`. Webhooks chegam fora de ordem. - O efeito no dinheiro e a atualização de `estado_dracma` vão na **mesma transação** do seu banco. Assim o seu front nunca vê `paid` antes do crédito. - O polling do front **não** deve chamar o Dracma a cada pedido. Leia o seu banco. Se um registro está parado em `creating` ou `pending` há mais de cerca de 60 segundos, faça **um** fetch-back por id (no máximo a cada 10 segundos por objeto), além do webhook e do job de `GET /v1/events`. ### Exemplos Um depósito do ponto de vista do jogador: | Instante | Dracma | Seu produto | Jogador vê | |---|---|---|---| | 15:00:00 | `POST /v1/deposits` sem resposta (timeout) | `creating` | "Gerando seu PIX..." | | 15:00:02 | `pending`, `version` 2 | `pending` | QR code e copia-e-cola | | 15:03:10 | `paid`, `version` 3 | `paid` (depois do crédito gravado) | "Depósito confirmado." | ### Erros e casos de borda - Estado desconhecido nunca move dinheiro. Mantenha a reserva do saque e não credite o depósito. - `refunded` e `chargeback` acontecem **depois** de o jogador ter visto `paid`. A tela não volta atrás; o caso vai para a fila de exceções (veja [Política de exceções de dinheiro](politica-de-excecoes.md)). - Um depósito `expired`, `cancelled` ou `failed` com `paid_amount` preenchido é pagamento tardio: não credite. ### Veja também - [Política de exceções de dinheiro](politica-de-excecoes.md) - [Ciclo do depósito](../dinheiro/ciclo-do-deposito.md) - [Ciclo do saque](../dinheiro/ciclo-do-saque.md) - [Modelo de dinheiro](../dinheiro/README.md) - [Webhooks: receptor passo a passo](../webhooks/receptor.md) --- # Arquivo: docs/conector/politica-de-excecoes.md ## Política de exceções de dinheiro > Para: Fin · Dev — Conferido em 2026-10-18. Alguns desfechos mexem em dinheiro que o seu produto já entregou ao jogador: estorno, chargeback, devolução de saque, pagamento tardio. Para esses casos o conector **não move dinheiro sozinho**. Ele abre um caso numa fila e uma pessoa decide. Esta página dá o modelo da fila, a tabela de situações e o fluxo de decisão. ### O que é Há dois conceitos parecidos, com donos diferentes: | | `under_review` | Fila de exceções (`needs_decision`) | |---|---|---| | Onde vive | Estado do objeto **no Dracma** | Fila **no seu produto** | | Quem decide | Revisor no painel do Dracma | Um administrador no seu backoffice | | O que decide | Se o dinheiro do Dracma segue adiante (aprovar, devolver, recusar) | Se a **carteira do jogador** muda (creditar, debitar, recreditar) quando não há regra automática segura | | Dinheiro enquanto espera | Nenhum movimento: depósito não credita e saque mantém a reserva | Nenhum movimento | O nome `needs_decision` é uma sugestão. O que importa é a fila existir, ter dono e gerar alerta. ### Quando usar Abra um caso nas situações da tabela abaixo. Em **nenhuma** delas o seu conector move dinheiro sozinho. ### Como funciona #### Tabela de situações | Situação | Default seguro | Ações humanas possíveis | |---|---|---| | Depósito já creditado vai a `refunded` ou `chargeback` | Não debita. Abre caso | Encerrar sem mover (a perda fica com a casa) ou débito manual e auditado na carteira | | Saque pago vai a `returned` (banco devolveu) | Não recredita. Abre caso. O dinheiro voltou ao caixa da operação no Dracma | Recreditar pela carteira (auditado) ou encerrar sem mover. Avisar o jogador para cadastrar outra chave | | Depósito pago depois de `expired`, `cancelled` ou `failed` (`paid_amount` num estado terminal) | Não credita. Abre caso | Creditar **só se** o Dracma estiver em `paid` naquele momento; senão encerrar e pedir ao suporte do Dracma o destino do valor | | `409` no envio de um saque (`idempotency_key_reused`, ou `duplicate_external_id` com corpo diferente) | Mantém a reserva. Nada é devolvido nem reenviado | Investigar. Devolver a reserva só com prova de que o dinheiro não está no PSP | | Fetch-back incoerente (`external_id` desconhecido, ou `amount`, `currency` ou `player_ref` diferentes do que você gravou) | Não credita. Alerta crítico | Investigar: pode ser bug ou ataque | | Seu envio esgotou as tentativas e a consulta por `external_id` não resolveu | Mantém a reserva | Reprocessar com a **mesma** `Idempotency-Key`. Devolver só com prova de que o saque não existe | | `recalled` ou `paid_partial` (estados do contrato que o PIX não usa) | Não mexe | Tratar como `returned` se ocorrer, após confirmar com o suporte | #### Fluxo: quando mover dinheiro sozinho ```mermaid flowchart TD A["Estado lido por fetch-back"] --> B{"Estado conhecido,
com regra escrita?"} B -->|"não"| Z["200 ao webhook, sem efeito,
confere depois"] B -->|"sim"| C{"Move dinheiro a favor do jogador,
sem risco?
paid (depósito),
rejected, failed, cancelled (saque)"} C -->|"sim"| D["Aplica, idempotente por external_id"] C -->|"não: refunded, chargeback, returned,
pago após expirar"| E["Abre caso na fila
+ alerta"] ``` Só duas famílias de efeito são automáticas: 1. **Depósito `paid`:** credita uma vez. 2. **Saque `rejected`, `failed` ou `cancelled`:** devolve a reserva ao jogador. E `paid` finaliza a reserva. Todo o resto vai para uma pessoa. O motivo: um débito automático em jogador que já sacou deixa saldo negativo, e um recrédito automático de saque devolvido pode pagar duas vezes se o banco reenviar. Esses casos são raros e caros. #### O caso na fila Registre em cada caso, no mínimo: | Campo | Para quê | |---|---| | Tipo da situação | Escolher a ação possível | | `id` do Dracma e `external_id` | Achar o objeto e a história (`GET /v1/events?object_id=`) | | Estado e `version` do Dracma | Saber o que foi aplicado | | Valor e jogador | Decidir | | `trace_id` | Falar com o suporte | | Quem abriu, quando, quem decidiu, motivo e desfecho | Auditoria | #### Ações da fila | Ação | Quando | Regra | |---|---|---| | `credit` | Depósito, só com o Dracma em `paid` agora | Credita uma vez, idempotente | | `refund` | Saque, só com o dinheiro fora do PSP ou devolvido ao caixa | Recredita o jogador, auditado | | `confirm_paid` | Saque cujo `paid` você não viu | Finaliza a reserva | | `close` | Qualquer caso | Encerra **sem mover dinheiro** | Recomendações para a fila: - Ler a fila exige uma permissão; decidir exige outra, mais restrita, com reautenticação (step-up) e **motivo obrigatório**. - Alerte caso parado: um aviso depois de algumas horas e um alerta crítico depois de um dia. Os prazos são seus. - Nunca decida sem consultar o objeto pela API no momento da decisão. ### Exemplos Um saque de R$ 50,00 pago e depois devolvido pelo banco: 1. O Dracma envia `withdrawal.returned`. Você lê `GET /v1/withdrawals/{id}`: `status: "returned"`, `return.returned_amount` igual a R$ 50,00. 2. O seu conector **não** recredita. Abre o caso "saque devolvido", com o valor devolvido e o jogador. 3. O seu produto mostra "Saque em análise". 4. Um administrador decide: recreditar (com motivo e auditoria) ou encerrar. O caixa da operação no Dracma já recebeu o valor devolvido. 5. Se o administrador recreditar, o jogador é avisado para cadastrar outra chave. ### Erros e casos de borda - **Evento repetido.** O mesmo desfecho pode chegar mais de uma vez. Abra um caso por objeto e por tipo, não por evento. - **Devolução parcial.** Credite, se decidir recreditar, só o valor de `return.returned_amount`. - **Caso parado.** Alerte. Um caso esquecido deixa o jogador sem resposta e o financeiro com diferença. - **Reserva sem desfecho.** Se o seu envio perdeu a resposta, nunca devolva a reserva: consulte `GET /v1/withdrawals?external_id=` antes. ### Veja também - [Estados no seu produto](estados-no-produto.md) - [Devoluções, estornos e disputas](../dinheiro/devolucoes-estornos-e-disputas.md) - [Ciclo do saque](../dinheiro/ciclo-do-saque.md) - [Ciclo do depósito](../dinheiro/ciclo-do-deposito.md) - [Idempotência e repetição](../api/idempotencia.md) - [Runbook de dinheiro](../dinheiro/runbook.md) --- # Arquivo: docs/conector/arquitetura.md ## Arquitetura do conector > Para: Dev — Conferido em 2026-10-18. O conector é o serviço do seu lado que fala com o Dracma. Esta página mostra como desenhá-lo para que uma falha de rede, um reinício ou uma queda do Dracma nunca dupliquem nem percam dinheiro, e nunca derrubem o seu produto. ### O que é O **conector** é o único serviço do operador que guarda a credencial do Dracma e chama a API. O resto do seu produto (front, portal, carteira) fala com o conector, nunca com o Dracma. ```mermaid flowchart LR J["Jogador"] --> P["Seu produto: front e portal"] P --> C["Conector"] C <--> L["Seu livro: carteira do jogador"] C -->|"chamada assinada com Idempotency-Key"| D["API v1 do Dracma"] D -->|"webhook assinado"| C C -->|"fetch-back, GET /events, GET /reconciliation"| D ``` O conector é dono de quatro coisas: 1. **A credencial** (chave e segredo), guardada num arquivo legível só pelo usuário do serviço ([Segredos e vazamento](../seguranca/segredos-e-credenciais.md#onde-guardar)). 2. **O outbox**: toda chamada de dinheiro ao Dracma nasce como uma linha gravada **antes** da chamada. 3. **O aplicador de estado**: um único caminho de código que transforma "o Dracma diz que isto aconteceu" em efeito no seu livro. 4. **Os jobs**: recuperação de eventos, reconciliação diária e alertas. ### Quando usar Use este desenho em qualquer produto que mova saldo de jogador com base no Dracma. Se o seu produto já tem um serviço de pagamentos, ele é o conector: acrescente o outbox e o aplicador único em vez de criar um segundo serviço. ### Como funciona #### Outbox e reserva na mesma transação O Dracma garante que **a mesma chave** nunca cria dois objetos (a resposta de `2xx` e `4xx` fica guardada por 30 dias; `5xx` e `429` liberam a chave). Mas só você sabe qual chave usar de novo depois de um reinício. Por isso, o intento é gravado antes da chamada: | Coluna sugerida | Conteúdo | |---|---| | `external_id` | `dep_` ou `wdr_`; é também a `Idempotency-Key` | | `kind` | `deposit` ou `withdrawal` | | `player_ref`, `amount_minor`, `currency` | O que foi pedido. É contra isso que o fetch-back é conferido | | `body` | Os **bytes exatos** do corpo enviado. A repetição manda os mesmos bytes | | `state` | `pending`, `acknowledged`, `rejected` ou `unknown` | | `attempts`, `next_attempt_at`, `last_trace_id` | Controle de repetição e diagnóstico (`Dracma-Trace-Id`) | | `dracma_id`, `dracma_status`, `dracma_version` | Último estado aplicado: id do Dracma e `version` monotônica | ```mermaid stateDiagram-v2 [*] --> pending: gravado na transação do pedido (e da reserva, no saque) pending --> acknowledged: 2xx ou Idempotent-Replayed pending --> rejected: 4xx conclusivo pending --> pending: timeout, 5xx, 429 ou idempotency_in_progress, com backoff e a mesma chave pending --> unknown: tentativas esgotadas sem resposta conclusiva unknown --> acknowledged: GET por external_id achou o objeto unknown --> needs_decision: GET por external_id inconclusivo rejected --> [*] acknowledged --> [*] ``` Regras do outbox: - **No saque, a reserva do saldo do jogador e a linha do outbox entram na mesma transação do seu banco.** Assim não existe saque chamado sem reserva, nem reserva sem registro de que o saque deve ser chamado. - A reserva só volta ao jogador em estado **conclusivo**: `rejected`, `failed` ou `cancelled` lido por fetch-back, ou `4xx` conclusivo na criação. Timeout, `5xx` e `429` **não** devolvem reserva: você não sabe se o saque foi criado. Devolver nesse caso paga o jogador duas vezes. - Backoff exponencial com jitter, respeitando o `Retry-After`. Cada tentativa é uma requisição nova (timestamp, nonce e assinatura novos), com a **mesma** chave e os **mesmos** bytes. Veja o helper de repetição em [Exemplos executáveis](../exemplos/README.md). - Um worker que reinicia retoma do outbox com a mesma chave. Nunca duplica. - Esgotou as tentativas: consulte `GET /v1/deposits?external_id=` ou `GET /v1/withdrawals?external_id=`. Achou, siga pelo objeto. Não achou ou inconclusivo, abra uma decisão manual ([Política de exceções de dinheiro](politica-de-excecoes.md)). Nunca decida às cegas. #### Se o Dracma cair, o seu produto continua A indisponibilidade do Dracma é uma situação normal de operação, não uma falha do seu produto. O conector tem de subir e funcionar sem o Dracma, e o resto do produto (login, jogo, carteira) tem de continuar de pé. | Situação | O que o conector faz | O que o jogador vê | |---|---|---| | Dracma responde `503`, `429` ou não responde, na criação de **depósito** | Grava o intento, repete com a mesma chave, devolve ao front um estado de "criando" | "Gerando seu PIX..." e polling do estado **local** | | Dracma indisponível na criação de **saque** | Mantém a reserva, repete com a mesma chave | "Saque em processamento." | | Webhook não chega | O job de `GET /v1/events` recupera ([Recuperação com GET /events](../webhooks/recuperacao-get-events.md)) | Nada; o estado só atrasa | | O conector reinicia no meio de uma chamada | O worker retoma do outbox | Nada | | Operação Real ainda sem credencial (antes do go-live) | O conector sobe, e as rotas de dinheiro respondem "indisponível" do seu produto | "Depósito indisponível no momento." | - **Nunca** faça o boot do conector depender de uma chamada ao Dracma. Valide a configuração, abra o banco e suba; carregue `GET /v1/currencies` e `GET /v1/rules` em cache com tolerância a falha. - Falha ao chamar o Dracma: **persista**, **registre com o `trace_id`**, **alerte** e **retome**. Não perca o intento e não trave os outros pagamentos por causa de um. - O polling do front lê o estado local do conector. Ele não é um proxy do Dracma: o front nunca bate no Dracma. Você pode ensaiar tudo isso com a queda simulada ([Receita 9](../sandbox/receitas.md#receita-9-queda-e-retomada-sem-duplicar)). #### Jobs | Job | Cadência | O que faz | Detalhe | |---|---|---|---| | Receptor do webhook | Contínuo | Verifica, deduplica, faz o fetch-back e aplica | [Receptor passo a passo](../webhooks/receptor.md) | | Poller de `GET /v1/events` | A cada 5 minutos | Recupera o que o webhook perdeu, pelo mesmo aplicador | [Exemplos executáveis](../exemplos/README.md) | | Worker do outbox | Contínuo | Repete as chamadas pendentes com a mesma chave | Acima | | Reconciliação diária | Uma vez por dia, depois do fechamento do dia UTC | Compara o relatório do Dracma com o seu livro | [Reconciliar com o seu livro](../dinheiro/reconciliacao-do-operador.md#rotina-diaria-pseudocodigo) | | Verificação de relógio | Periódica | `GET /v1/ping` e alerta com desvio acima de 5 s | [Alertas do conector](alertas-e-observabilidade.md) | Os quatro primeiros terminam no **mesmo** aplicador de estado: dedup por id do evento, fetch-back, conferência com o intento, `version` maior que a aplicada, efeito idempotente por `external_id`. Um só lugar para a regra de dinheiro evita que o webhook e o job discordem. #### Regras de bônus ficam fora da API O Dracma não conhece bônus, rollover, promoção nem cashback. O corpo de `POST /v1/deposits` e de `POST /v1/withdrawals` leva só o que o Dracma precisa decidir e mover: valor, jogador, método e destino. Campo desconhecido é recusado com `400 validation_failed` e `reason: unknown_field`. - Se o seu front manda uma opção de bônus no depósito (por exemplo, "aceitar bônus"), **guarde-a no seu intento** e **não** a envie ao Dracma. - Aplique a regra de bônus no seu livro depois que o depósito estiver `paid` e creditado, como um segundo lançamento seu, idempotente pelo mesmo `external_id`. - Regras de saque ligadas a bônus (rollover, saldo bloqueado) são checadas por você **antes** de reservar. Se quiser que o Dracma mande o saque para revisão por desconfiança sua, use `review_requested: true`, que só torna a decisão mais restritiva ([Decisão, limites e regras](../dinheiro/decisao-limites-e-regras.md)). #### Quem garante o quê | Garantia | Quem | |---|---| | Não criar duas cobranças nem dois saques para a mesma chave | Dracma, desde que você repita com a mesma chave | | Decidir saque automático, revisão ou bloqueio | Dracma | | Creditar uma vez, e só em `paid` lido por fetch-back | Você | | Reservar antes e devolver só em estado conclusivo | Você | | Não perder evento | Os dois: o Dracma reentrega e mantém `GET /v1/events`; você roda o job | | Provar que os dois livros batem | Os dois: reconciliação diária com digest assinado | ### Exemplos Depósito, do pedido do jogador ao crédito: ```mermaid sequenceDiagram autonumber participant J as Jogador participant C as Conector participant O as Outbox e livro participant D as Dracma J->>C: pede depósito de 50,00 C->>O: grava intento (external_id, valor em centavos) na transação C->>D: POST /v1/deposits (Idempotency-Key = external_id) D-->>C: 201 pending com instructions.pix C-->>J: PIX para pagar D->>C: webhook deposit.paid (assinado) C->>C: verifica assinatura e deduplica C->>D: GET /v1/deposits/{id} (fetch-back) D-->>C: status paid, valor e jogador C->>O: confere com o intento e credita uma vez, na mesma transação do dedup C-->>D: 200 ``` Os exemplos prontos, com retry, parser de erro, paginação e poller, estão em [Exemplos executáveis](../exemplos/README.md). Um caso real, com este desenho completo, está na seção [Casos](../casos/README.md). ### Erros e casos de borda | Situação | Tratamento | |---|---| | O processo morre entre o commit do intento e a chamada | O worker repete do outbox com a mesma chave: 1 objeto | | A resposta `201` se perde | A repetição devolve a mesma resposta com `Idempotent-Replayed: true` | | `409 duplicate_external_id` | O objeto já existe: siga pelo `existing` da resposta | | `409 idempotency_key_reused` | Bug do conector (mesma chave, corpo diferente): abra decisão manual e alerte; não devolva reserva nem reenvie | | `409 idempotency_in_progress` | Outra execução segura a chave: repita depois do `Retry-After` | | `4xx` de validação | O objeto não existe: marque `rejected`, devolva a reserva (saque) e corrija. Nova tentativa depois da correção usa um **novo** `external_id` | | Estado desconhecido no fetch-back | Não move dinheiro; registre e trate como "em análise" | ### Veja também - [Estados no seu produto](estados-no-produto.md): como traduzir cada estado do Dracma para o seu front. - [Política de exceções de dinheiro](politica-de-excecoes.md): quando nunca mover dinheiro sozinho. - [Alertas do conector](alertas-e-observabilidade.md). - [Idempotência e repetição](../api/idempotencia.md) e [Receptor passo a passo](../webhooks/receptor.md). - [Casos](../casos/README.md): um conector real com este desenho. --- # Arquivo: docs/conector/alertas-e-observabilidade.md ## Alertas do conector > Para: Dev — Conferido em 2026-10-18. O que medir e alertar no seu conector para saber, em minutos, se o dinheiro do seu produto e o do Dracma continuam batendo. Os limiares abaixo são pontos de partida: ajuste ao seu volume. ### O que é Um conjunto mínimo de sinais de **saúde do dinheiro** e de **saúde do receptor**. Cada sinal responde a uma pergunta: o dinheiro está parado? Está duplicado? O Dracma e o meu livro divergiram? O meu receptor está de pé? O Dracma tem os próprios alertas por operação (entregas em DLQ, caixa baixo, regras em revisão), descritos em [Alertas e e-mails](../painel/alertas.md). Os alertas desta página são **seus**: só o conector sabe, por exemplo, que um intento ficou pendente por tempo demais. ### Quando usar Configure-os antes do go-live ([Checklist de go-live](../go-live/checklist.md)) e dispare cada um de teste uma vez, para provar que o aviso chega a uma pessoa. Alerta que nunca foi disparado de teste não conta como configurado. ### Como funciona #### Saúde do dinheiro | Sinal | Limiar sugerido | Severidade | O que significa e o que fazer | |---|---|---|---| | Intento do outbox pendente há mais que | 10 minutos | P2 | O Dracma está fora ou a chamada está falhando. Veja os `5xx` e `429`; confira `GET /v1/ping`. O worker já repete com a mesma chave | | Fila de decisão manual (`needs_decision`) com caso parado | Mais de 4 horas: aviso. Mais de 24 horas: crítico | P2, depois P1 | Um humano precisa decidir um estorno, devolução ou pagamento tardio ([Política de exceções](politica-de-excecoes.md)) | | Fetch-back incoerente com o intento (id, valor, moeda ou jogador diferentes) | Qualquer ocorrência | P1 | Pode ser bug ou ataque. Não credite; investigue pelo `trace_id` | | Reconciliação diária com divergência | Qualquer divergência | P1 | Os livros não batem. **Não** "conserte" o seu livro para bater: congele e investigue ([Reconciliar com o seu livro](../dinheiro/reconciliacao-do-operador.md#investigar-uma-diferenca)) | | Crédito sem depósito `paid` correspondente | Qualquer ocorrência | P1 | É o desvio mais grave: dinheiro creditado que o Dracma não liquidou | | Mais de um crédito para o mesmo `external_id` | Qualquer ocorrência | P1 | A restrição de unicidade do seu livro deveria impedir; se aparecer, o dedup falhou | | Caixa livre da operação abaixo do mínimo que você define | Defina pelo seu volume de saques | P2 | `GET /v1/balances`: caixa livre é `available` menos `pending_out`. Saque sem caixa recebe `422 insufficient_operation_balance` ([Saldos e caixa livre](../dinheiro/saldos.md#saque-sem-caixa-livre)) | | Saques em `processing` há muito tempo | Defina pelo seu SLA com o jogador | P2 | O PSP pode estar lento; o Dracma não reenvia sozinho um saque em andamento ([Ciclo do saque](../dinheiro/ciclo-do-saque.md)) | #### Saúde do receptor e da integração | Sinal | Limiar sugerido | Severidade | O que significa e o que fazer | |---|---|---|---| | Webhook com assinatura inválida | Mais de 0 em 5 minutos | P2 | Ataque, ou rotação de segredo mal feita. Confira os segredos ativos ([Segredo e rotação](../webhooks/segredos-e-rotacao.md)) | | `401` ou `403` nas chamadas do conector ao Dracma | Mais de 0 | P2 | Credencial, relógio ou operação suspensa. `403 operation_suspended` pede o suporte | | Desvio de relógio (`server_time` do `GET /v1/ping`) | Mais de 5 segundos | P2 | Fora de 300 s todas as chamadas dão `401`. Confira o NTP | | Job de `GET /v1/events` sem sucesso | Mais de 15 minutos | P2 | Webhooks perdidos não estão sendo recuperados | | Atraso do poller (agora menos `created_at` do último evento aplicado) | Defina; 15 minutos é um bom começo | P2 | O conector está atrasado em relação ao Dracma | | Fração de `5xx` e `429` do Dracma nas suas chamadas | Defina pela sua linha de base | P2 | `429` é o seu próprio ritmo; `5xx` persistente é incidente do lado do Dracma ([Limites e quotas](../api/limites.md#baldes-de-taxa)) | | Entregas de webhook em `dlq` ou em tentativas altas | Qualquer entrega em `dlq` | P2 | O seu receptor ficou fora do ar por mais de 72 horas, ou está recusando. Reentregue pelo painel ([Webhooks no painel](../painel/webhooks.md#aba-dlq)) | #### Métricas sugeridas Nomes de exemplo para as métricas do seu conector; use a convenção do seu produto. | Métrica | Tipo | Rótulos | |---|---|---| | `conector_dracma_outbox_pendentes` e `conector_dracma_outbox_idade_maxima_segundos` | Gauge | `kind` (`deposit`, `withdrawal`) | | `conector_dracma_chamadas_total` | Counter | `rota`, `status` | | `conector_dracma_webhook_verificacao_total` | Counter | `resultado` (`ok`, `assinatura_invalida`, `fora_da_janela`, `id_divergente`, `duplicado`) | | `conector_dracma_eventos_atraso_segundos` | Gauge | nenhum | | `conector_dracma_fetchback_divergente_total` | Counter | `tipo` | | `conector_dracma_decisoes_abertas` e `conector_dracma_decisoes_idade_maxima_segundos` | Gauge | `motivo` | | `conector_dracma_reconciliacao_divergencias` | Gauge | `dia` | | `conector_dracma_desvio_relogio_segundos` | Gauge | nenhum | #### Logs e rastreio - Registre o `Dracma-Trace-Id` de **toda** resposta de erro, e o `trace_id` do corpo do `problem+json`. É o que o suporte pede ([Suporte](../ajuda/suporte.md)). - Em todo registro de dinheiro inclua `external_id`, o id do Dracma, o `Dracma-Event-Id` e a `version` aplicada. - **Nunca** registre o segredo, o header `Dracma-Signature` inteiro, CPF completo nem chave PIX em claro. - Registre a decisão do aplicador de estado ("aplicado", "duplicado", "versão antiga", "incoerente"). Sem isso, uma divergência de reconciliação não tem como ser explicada. ### Exemplos Verificação de relógio, para rodar a cada minuto e alertar com desvio acima de 5 segundos (exemplo em Go, do pacote de [Exemplos executáveis](../exemplos/README.md)): ```bash cd exemplos/go && DRACMA_KEY_ID=key_test_SEU_ID DRACMA_KEY_SECRET_FILE=/caminho/seguro/segredo go run ./cmd/ping ``` A saída traz `desvio=` e o programa avisa no erro padrão quando o desvio passa de 5 segundos. Regra de alerta para o outbox, em pseudoconfiguração: ```text alerta: conector_dracma_outbox_idade_maxima_segundos{kind="withdrawal"} > 600 por: 2 minutos severidade: P2 mensagem: saque pendente há mais de 10 minutos; veja o trace_id do último erro ``` ### Erros e casos de borda - Alerta com ruído é alerta ignorado: se o P2 do outbox dispara toda vez que o Dracma fica 11 minutos fora, aumente o tempo ou agrupe. - Não alerte por `429` isolado: ele é o limitador funcionando. Alerte pela taxa. - Reconciliação com divergência no dia de hoje durante a madrugada pode ser só o dia ainda aberto: `409 day_open` não é divergência ([Reconciliação](../referencia/reconciliacao.md#disponibilidade-e-409-day_open)). - Um alerta crítico de dinheiro deve acordar alguém. Um P2 pode esperar o horário comercial. ### Veja também - [Arquitetura do conector](arquitetura.md). - [Política de exceções de dinheiro](politica-de-excecoes.md). - [Runbook de dinheiro](../dinheiro/runbook.md): sintoma, diagnóstico e ação. - [Alertas e e-mails](../painel/alertas.md): os alertas que o Dracma envia. - [Problemas comuns de webhook](../webhooks/troubleshooting.md). --- # Arquivo: docs/webhooks/README.md ## Visão geral > Para: Dev — Conferido em 2026-10-18. O Dracma avisa cada mudança de estado de um depósito ou saque com um `POST` assinado na URL de webhook da sua operação. O webhook **só avisa**: quem decide se o dinheiro se move é o estado que a API devolve quando você consulta o objeto. Esta página resume o que o Dracma garante, o que ele não garante e como as páginas desta seção se encaixam. ### O que é Um webhook é uma requisição `POST` do Dracma para o seu servidor, com um [envelope](envelope.md) JSON que descreve uma transição de estado (`deposit.paid`, `withdrawal.failed` e assim por diante). Você cadastra **uma URL por operação**; os eventos de uma operação de teste vão só para a URL da operação de teste, e os da operação real só para a URL da real. As garantias de entrega são estas: - **Pelo menos uma vez (at-least-once).** O mesmo evento pode chegar mais de uma vez, sempre com o mesmo `Dracma-Event-Id` e os mesmos bytes de corpo. A deduplicação por esse id é obrigação do seu receptor. - **O Dracma grava antes de tentar.** O evento é registrado de forma durável antes da primeira tentativa de envio, e uma tentativa que falha é repetida pelo cronograma de [Retentativas, skipped e DLQ](retentativas-e-dlq.md#cronograma-e-jitter). Uma indisponibilidade momentânea do Dracma não perde o evento. - **Sucesso é uma resposta 2xx** em até 10 s. Qualquer outra coisa (4xx, 5xx, timeout, redirect, erro de rede ou de TLS) conta como falha e gera nova tentativa por até 72 h. - **Todo evento fica disponível em `GET /events`**, com o mesmo envelope, mesmo que o webhook nunca tenha chegado. Veja [Recuperação com `GET /events`](recuperacao-get-events.md). ### Quando usar Use webhooks como o **gatilho rápido** da sua integração: eles dizem que algo mudou e qual objeto consultar. Eles não substituem a consulta à API, o job de recuperação nem a reconciliação diária. A integração completa tem três camadas: | Camada | Para que serve | Página | |---|---|---| | Webhook | Reagir em segundos a cada mudança de estado. | Esta seção | | Job de `GET /events` a cada 5 minutos | Fechar lacunas: webhook perdido, DLQ, queda do seu receptor, eventos não assinados. | [Recuperação com `GET /events`](recuperacao-get-events.md) | | Reconciliação diária | Provar que o seu livro e o do Dracma fecham. | [Reconciliar com o seu livro](../dinheiro/reconciliacao-do-operador.md) | #### O que o Dracma não garante - **Ordem.** Cada transição gera um webhook próprio, e as entregas de eventos diferentes competem entre si. Um `deposit.paid` pode chegar antes de um `deposit.pending` atrasado. O contador `object_version`, que só cresce, resolve a ordem de um mesmo objeto. - **Estado atual no corpo.** O `data.object` é o estado **no momento da transição**, congelado. Uma reentrega feita horas depois leva o objeto antigo. - **Um webhook por mudança, sem junção.** O Dracma não junta transições rápidas em um único webhook. Cada transição tem o seu evento; o que pode parecer "junção" é só a chegada fora de ordem. ### Como funciona ```mermaid sequenceDiagram participant P as Dracma (API) participant W as Dracma (webhooks) participant R as Seu receptor P->>P: Transição de estado (grava o evento junto) P->>W: Evento novo W->>W: Grava a entrega (corpo congelado) W->>R: POST assinado (Dracma-Event-Id, Dracma-Signature) R->>R: Verifica assinatura, deduplica R->>P: GET /v1/deposits/{object_id} (sua credencial) P-->>R: Estado atual R->>R: Aplica o efeito, idempotente pelo external_id R-->>W: 200 ``` 1. A transição do objeto grava o evento na mesma transação que muda o estado. Por isso `GET /events` nunca perde uma transição. 2. O Dracma registra a entrega com o corpo já montado e assinado a cada tentativa. 3. O seu receptor verifica a assinatura sobre os bytes crus, deduplica pelo `Dracma-Event-Id` e **consulta o objeto na API com a sua credencial** (o *fetch-back*). 4. O receptor aplica o efeito pelo estado devolvido pela consulta e responde 2xx. #### Um webhook forjado não cria dinheiro Se o seu segredo de webhook vazar, alguém consegue montar um `deposit.paid` com assinatura válida. Como o seu receptor **não credita pelo corpo** e sim pelo `GET` feito com a **sua** credencial de API, o webhook forjado vira uma consulta inútil: o depósito segue no estado que o Dracma realmente tem. É por isso que o fetch-back é obrigatório e não uma otimização. A suíte de conformidade testa exatamente isto: um webhook forjado com assinatura válida não pode gerar crédito. #### Glossário | Termo | Significado | |---|---| | Entrega | O registro de um evento a enviar para a URL da operação. Há uma entrega por evento. | | Tentativa | Um `POST` dessa entrega. Uma entrega tem até 15 ou 16 tentativas por ciclo. | | Estados da entrega | `pending` (aguarda a primeira tentativa ou a próxima), `delivered` (recebeu 2xx), `dlq` (esgotou as tentativas) e `skipped` (nada foi enviado). | | DLQ | Fila das entregas que esgotaram as 72 h sem 2xx. O evento não se perde: continua em `GET /events` e pode ser reentregue pelo painel. | | `skipped` | Entrega registrada, mas não enviada: sem URL cadastrada, ambiente diferente ou tipo não assinado. | | Fetch-back | A consulta à API que o receptor faz com a própria credencial para confirmar o estado antes de agir. | ### Exemplos Um caminho feliz de depósito, do ponto de vista do receptor: | Passo | O que acontece | |---|---| | 1 | Chega `deposit.pending` (`object_version` 2). Você exibe o QR Code. | | 2 | Chega `deposit.paid` (`object_version` 3). Você consulta `GET /v1/deposits/{object_id}`, confere `external_id`, `amount` e `player_ref` e credita o jogador **uma única vez**. | | 3 | O Dracma repete `deposit.paid` por falha de rede do seu lado. O `Dracma-Event-Id` já foi processado: você responde 200 sem efeito. | O código completo de cada passo está em [Receptor passo a passo](receptor.md). ### Erros e casos de borda - **Nunca receber nada.** Confira se a URL está cadastrada e efetiva (a alteração tem aprovação e espera), se o tipo está assinado e se o ambiente da URL é o da operação. Veja [Problemas comuns](troubleshooting.md). - **Receber duplicado.** É esperado; responda 200 sem efeito. - **Receber fora de ordem.** É esperado; ordene por `object_version` e aja pelo estado da consulta. - **Responder erro de propósito.** Responder 4xx ou 5xx não "cancela" o evento: o Dracma repete por até 72 h. Para um evento que você não quer tratar, responda 200. ### Veja também - [Seu endpoint](endpoint-e-politica-de-url.md): regras da URL e hosts permitidos. - [Requisição e assinatura](requisicao-e-assinatura.md): headers e vetores de teste. - [Envelope](envelope.md) e [Catálogo de eventos](catalogo-de-eventos.md): o que chega. - [Receptor passo a passo](receptor.md#os-13-passos-nesta-ordem): a ordem de verificação. - [Retentativas, skipped e DLQ](retentativas-e-dlq.md): cronograma e recuperação. - [Recuperação com `GET /events`](recuperacao-get-events.md): a rede de segurança. - [Segredo e rotação](segredos-e-rotacao.md): ciclo de vida do `whsec_`. - [Problemas comuns](troubleshooting.md): sintoma, causa e o que mandar ao suporte. --- # Arquivo: docs/webhooks/endpoint-e-politica-de-url.md ## Seu endpoint > Para: Dev · Adm — Conferido em 2026-10-18. O Dracma só envia webhooks para a URL que você cadastra na operação, e só se ela passar na política de URL: HTTPS na porta 443, nome de domínio público, sem credencial na URL e sem redirect. Esta página lista as regras, explica o que acontece quando um endereço é recusado e diz como liberar um domínio fora de produção. ### O que é Cada operação tem **uma** URL de webhook. A política abaixo vale no cadastro e é **conferida de novo antes de cada tentativa de entrega**. | Regra | Detalhe | |---|---| | Esquema | Somente `https`. `http` é recusado. | | Porta | 443. Uma porta explícita diferente de 443 é recusada (`:443` explícito é aceito). | | Credenciais | Sem usuário nem senha na URL (`https://usuario:senha@host/...` é recusado). | | Fragmento | Sem `#fragmento`. | | Tamanho | Até 2048 caracteres, tanto como você digitou quanto depois de normalizada. | | Nome do host | Nome DNS em ASCII (domínios internacionais em `xn--`), com pelo menos dois rótulos de 1 a 63 caracteres, sem hífen nas pontas, até 253 caracteres no total. Sublinhado não é aceito. | | Nomes numéricos | O último rótulo não pode ser numérico (`127.1`, `0x7f.0.0.1` e `2130706433` são recusados). | | `localhost` | `localhost` e `*.localhost` são recusados. | | IP literal | Endereços em faixa não pública são recusados. Prefira sempre um nome de domínio: o certificado TLS costuma ser emitido para nomes. | | Redirect | O Dracma **não segue redirect**. Um `3xx` conta como falha. Cadastre a URL final. | !!! note "Certificado" O Dracma valida o certificado do seu servidor como um navegador: cadeia completa de uma autoridade pública e nome do host no certificado. Certificado autoassinado, vencido ou com cadeia incompleta faz a tentativa falhar. ### Quando usar Leia esta página antes de pedir a primeira URL e sempre que mudar o endereço do receptor (novo domínio, novo balanceador, migração de provedor). #### Quem cadastra e como !!! note "Confirmar após deploy" No onboarding, o Dracma cria o seu tenant e **convida o administrador da sua empresa por e-mail** (inclusive no ambiente de homologação, com e-mail real). Esse administrador torna-se o dono do tenant e, a partir daí, cria as próprias operações e chaves. O fluxo de convite está em construção; confirme o estado atual com o seu contato do Dracma. A URL é cadastrada de duas formas: 1. **Na criação da operação**, quando o Dracma cria a operação no onboarding com a URL que você informou. A política acima vale do mesmo jeito. 2. **Pelo painel**, na tela de webhooks ([Webhooks no painel](../painel/webhooks.md)): você propõe a URL e os tipos de evento, o painel valida a política (inclusive a resolução de DNS) sem gravar nada e abre um pedido. Outra pessoa com permissão de gerenciar webhooks precisa aprovar, e a mudança só passa a valer **24 horas depois da aprovação**. Quem propõe confirma a identidade de novo no painel (verificação adicional) antes de enviar. Enquanto a mudança espera, a URL anterior (se houver) continua valendo. #### Testar a URL !!! warning "Não disponível" O Dracma não prova que a URL é sua. Não existe ping, desafio (*challenge*) nem verificação de posse no cadastro: a validação só confere forma, política e DNS. Por isso um erro de digitação em um domínio que existe passa no cadastro, e os eventos seguem para o endereço errado até alguém notar. Para testar o receptor de verdade, use a operação de teste: gere um depósito, simule o pagamento e peça a reentrega do evento (veja [Sandbox](../sandbox/README.md)). O endereço de teste não substitui conferir, no painel, o log de entregas depois do primeiro evento real. ### Como funciona ```mermaid flowchart TD A[URL cadastrada] --> B{Forma válida?
https, 443, sem credencial} B -- não --> X[Recusada] B -- sim --> C{Host permitido neste ambiente?} C -- não --> X C -- sim --> D[Resolve o DNS] D --> E{Algum IP em faixa proibida?} E -- sim --> X E -- não --> F[Envia o POST] F --> G{2xx em até 10 s?} G -- sim --> H[Entregue] G -- não --> I[Nova tentativa pelo cronograma] ``` #### Proteção contra acesso a redes internas O Dracma resolve o nome do seu host no cadastro **e antes de cada tentativa**, e confere o endereço efetivamente usado na conexão. Se **qualquer** endereço resolvido cair em uma faixa que não é da internet pública, a tentativa falha. Ficam bloqueadas, entre outras: redes privadas (`10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`), loopback, link-local (inclusive o endereço de metadados de nuvem), CGNAT (`100.64.0.0/10`), faixas reservadas, de documentação e de multicast. No IPv6, só endereços de unicast global (`2000::/3`) passam. Consequência prática: o seu receptor precisa estar em um IP público roteável, atrás de um nome DNS estável. Se o nome passar a resolver para um endereço interno (por exemplo, um DNS de split-horizon), o cadastro já foi aceito, mas **cada tentativa falha** e o evento percorre o cronograma até a DLQ. Uma falha desse tipo aparece no log de entregas como erro sem código HTTP. #### Ambiente da URL O ambiente da URL é o da operação: `sandbox` (operação de teste) ou `production` (operação real). Ele não muda depois de criado. Cada evento nasce em um ambiente, e **só é enviado à URL da operação do mesmo ambiente**. Se os ambientes divergem, a entrega fica `skipped` com o motivo `environment_mismatch` e nada sai. Isso impede que um evento de teste chegue ao receptor de produção, e o inverso. Veja [Retentativas, skipped e DLQ](retentativas-e-dlq.md#skipped-o-evento-foi-registrado-e-nada-foi-enviado). #### Hosts permitidos fora de produção Nas instâncias do Dracma que não são a de produção (como a de homologação), o Dracma só envia webhooks para hosts de teste, mesmo que a operação seja real. Passam: - nomes sob os domínios reservados `.test`, `.invalid` e `.example`; - os domínios que o Dracma tiver liberado para você. Qualquer outro domínio é recusado no cadastro (o painel aponta o campo `url`) e, se já estiver cadastrado, a tentativa falha. Isso existe para que o ambiente de testes nunca dispare efeito em um sistema real por engano. Os domínios reservados não existem na internet pública, e o nome cadastrado também precisa **resolver no DNS** a partir do Dracma. Na prática, para receber webhooks de verdade fora de produção, você precisa que o seu domínio real seja liberado. **Para receber webhooks de teste no seu domínio real, peça a liberação no onboarding:** informe ao Dracma o domínio (ou o sufixo) que você quer usar fora de produção. A liberação é feita pelo Dracma; você não consegue ativá-la sozinho. !!! warning "Não disponível" Túneis públicos temporários (do tipo ngrok, localtunnel e semelhantes) e `localhost` não funcionam como destino de webhook fora de produção: o domínio do túnel não é um host de teste e não está liberado, e `localhost` é recusado pela política. Para desenvolver o receptor sem expor a máquina, simule a requisição com os [vetores](requisicao-e-assinatura.md) e as [receitas do sandbox](../sandbox/receitas.md). ### Exemplos | URL | Resultado | |---|---| | `https://pagamentos.operador.example/dracma/webhook` | Passa na política em qualquer instância (domínio reservado `.example`); o nome ainda precisa resolver no DNS. | | `https://pagamentos.operador.example:443/dracma/webhook` | Passa na política (porta 443 explícita). | | `https://pagamentos.operador.example:8443/dracma/webhook` | Recusada: porta diferente de 443. | | `http://pagamentos.operador.example/dracma/webhook` | Recusada: não é HTTPS. | | `https://usuario:senha@pagamentos.operador.example/webhook` | Recusada: credenciais na URL. | | `https://pagamentos.operador.example/webhook#x` | Recusada: fragmento. | | `https://localhost/webhook` | Recusada: `localhost`. | | `https://10.0.0.5/webhook` | Recusada: IP de rede privada. | | `https://127.1/webhook` | Recusada: nome numérico. | | `https://pagamentos.operador.com.br/dracma/webhook` | Aceita em produção; fora de produção só se o domínio tiver sido liberado. | ### Erros e casos de borda | Sintoma | Causa provável | O que fazer | |---|---|---| | O painel recusa o pedido apontando o campo `url` | A política acima (esquema, porta, credencial, host) ou o DNS do nome resolve para faixa bloqueada, ou o domínio não é de teste fora de produção. | Corrija a URL. Em instância que não é a de produção, peça a liberação do domínio no onboarding. | | A URL foi aceita, mas as entregas falham sem código HTTP | O nome passou a resolver para um IP bloqueado, o certificado é inválido ou o servidor não responde na porta 443. | Confira DNS, certificado e firewall. O log de entregas mostra o erro de cada tentativa. | | Entregas ficam `skipped` com `environment_mismatch` | O evento é de teste e a URL cadastrada é a da operação real (ou o inverso). | Cada operação tem a própria URL; cadastre a certa na operação certa. | | Mudança de URL "não pega" | O pedido ainda espera aprovação de outra pessoa ou as 24 h depois da aprovação. | Veja [Webhooks no painel](../painel/webhooks.md). | | O receptor está atrás de uma lista de IPs permitidos | O Dracma não publica endereços de saída fixos. | Veja o aviso abaixo. | #### IPs de saída do Dracma !!! warning "Não disponível" O Dracma não publica uma lista de endereços IP de saída dos webhooks: ela não está no painel nem em nenhuma rota da API, e o Dracma não garante IP fixo de origem. Textos antigos diziam que a lista estava "disponível no painel"; isso não é verdade. Não construa seu receptor dependendo de filtrar pelo IP de origem. A defesa do receptor é a **assinatura** sobre o corpo cru e o **fetch-back** autenticado, que valem de qualquer IP. Se a sua política de segurança exige uma lista de origens, hoje não há como atendê-la: o Dracma não oferece essa lista. Se um dia você receber uma lista por contrato, trate-a como uma camada **a mais**, que nunca substitui a verificação da assinatura. Dentro do seu serviço, confie em `X-Forwarded-For` somente quando o salto anterior for o seu proxy de borda. ### Veja também - [Webhooks no painel](../painel/webhooks.md): como propor, aprovar e acompanhar a URL. - [Retentativas, skipped e DLQ](retentativas-e-dlq.md): o que acontece quando a URL falha. - [Requisição e assinatura](requisicao-e-assinatura.md): o que o receptor recebe. - [Rede, allowlist e mTLS](../seguranca/rede-allowlist-e-mtls.md): a segurança de rede das chamadas à API. --- # Arquivo: docs/webhooks/requisicao-e-assinatura.md ## Requisição e assinatura > Para: Dev — Conferido em 2026-10-18. Cada webhook é um `POST` com corpo JSON, três headers `Dracma-*` e uma assinatura HMAC-SHA256 calculada sobre os bytes exatos do corpo. Esta página descreve a requisição que o seu receptor recebe e o cálculo da assinatura, com vetores de teste que toda implementação precisa reproduzir. ### O que é O Dracma envia o webhook como uma requisição `POST` para a URL cadastrada na sua operação (veja [Seu endpoint](endpoint-e-politica-de-url.md)). Toda tentativa leva: | Item | Valor | |---|---| | Método | `POST` | | `Content-Type` | `application/json; charset=utf-8` | | `User-Agent` | `Dracma-Webhooks/1` | | `Dracma-Event-Id` | Id do evento, no formato `evt_` seguido de 32 caracteres hexadecimais minúsculos. É **igual** ao campo `id` do corpo e é a chave de deduplicação. | | `Dracma-Timestamp` | Segundos Unix (UTC) do envio, em decimal. É **igual** ao `t=` de `Dracma-Signature`. | | `Dracma-Signature` | `t=,v1=`; durante uma rotação de segredo chegam dois `v1`. | O `User-Agent` identifica o emissor para logs e regras de proxy, mas **não é autenticação**: qualquer cliente pode enviá-lo. Quem autentica é a assinatura. O Dracma não envia nenhum header de rastreamento (`traceparent`, `tracestate`) ao seu receptor. Para pedir ajuda ao suporte, use `Dracma-Event-Id` e o `operation_id` do corpo (veja [Problemas comuns](troubleshooting.md)). ### Quando usar Leia esta página ao implementar a verificação do receptor e use os [vetores](#exemplos) como teste de aceitação antes de apontar a URL para o Dracma. A verificação da assinatura é o primeiro passo do receptor ([Receptor passo a passo](receptor.md)); ela vem **antes** de interpretar o JSON. ### Como funciona #### A assinatura ``` v1 = hex_minúsculo( HMAC-SHA256( segredo_de_webhook, "." + corpo_cru ) ) ``` - **`segredo_de_webhook`** é a string **inteira**, com o prefixo `whsec_`, usada como está (como bytes UTF-8) para chave do HMAC. Não remova o prefixo e não decodifique o restante como hexadecimal. - **``** é o valor decimal do `t=`, sem sinal e sem zeros à esquerda, seguido de um ponto (`.`). - **`corpo_cru`** são os **bytes exatos** recebidos. Calcule a assinatura antes de interpretar o JSON e nunca sobre um JSON serializado de novo, porque a reserialização muda os bytes. - O `v1` vem em hexadecimal minúsculo, com 64 caracteres. O segredo de webhook (`whsec_…`) é **diferente** do segredo de chamada da API. Guarde os dois no cofre, separados. O formato e o ciclo de vida estão em [Segredo e rotação](segredos-e-rotacao.md). #### O header `Dracma-Signature` O header é uma lista de pares `chave=valor` separados por vírgula: - Exatamente um `t=`, em forma canônica: só dígitos, sem sinal (`+`, `-`), sem espaços e sem zero à esquerda (o próprio `0` é válido). O Dracma nunca emite outra forma. - Um `v1=` por segredo ativo, **o mais novo primeiro**. São no máximo dois (veja abaixo). O Dracma gera um `t=` novo e uma assinatura nova em **cada tentativa** de entrega. Por isso uma reentrega de horas depois continua dentro da janela de 300 s: a janela vale para o `t=` da tentativa, não para a hora do evento. Hoje o Dracma emite só as chaves `t` e `v1`. Quando o header trouxer uma chave que você não conhece, **ignore-a**: ela pode ser um esquema de assinatura de versão futura. Rejeitar o header por causa de uma chave desconhecida quebraria o seu receptor no dia em que ela aparecer. #### Dois `v1` durante a rotação Cada operação pode ter até **dois segredos de webhook ativos** ao mesmo tempo: o mais novo e, por até 7 dias depois de uma rotação, o anterior. Enquanto os dois valem, o header traz dois `v1` (o do segredo novo primeiro). O seu receptor aceita se **qualquer** `v1` bater com **qualquer** segredo que ele conhece. Isso permite trocar o segredo sem perder entrega. O procedimento completo está em [Segredo e rotação](segredos-e-rotacao.md#sobreposicao-de-7-dias). #### O corpo é idêntico em toda tentativa O Dracma monta o corpo uma vez, quando o evento nasce, grava os bytes e **reenvia exatamente os mesmos bytes** em toda tentativa e em toda reentrega manual. O que muda de uma tentativa para a outra é só `Dracma-Timestamp`, o `t=` e a assinatura. Duas consequências práticas: - A deduplicação por `Dracma-Event-Id` é segura: o mesmo evento tem sempre o mesmo `id` e o mesmo conteúdo. - O `data.object` é o estado do objeto **no momento da transição**, congelado. Uma reentrega feita horas depois leva o objeto antigo. Por isso o receptor confirma o estado atual com uma consulta à API (o *fetch-back*), descrito em [Receptor passo a passo](receptor.md#fetch-back-por-que-e-obrigatorio). ### Exemplos #### Uma requisição completa ```http POST /dracma/webhook HTTP/1.1 Host: pagamentos.operador.example Content-Type: application/json; charset=utf-8 User-Agent: Dracma-Webhooks/1 Dracma-Event-Id: evt_01J9ZQ5 Dracma-Timestamp: 1790000300 Dracma-Signature: t=1790000300,v1=4f82d5dac571d1a7b4dae9ef852c74cffcb6a4ce76f7f3209d685798e02a229b {"id":"evt_01J9ZQ5","type":"deposit.paid","created_at":"2026-09-26T15:00:00Z","operation_id":"op_nero_hml","object_id":"dep_01J9ZQ3","object_version":3,"data":{"object":{"id":"dep_01J9ZQ3","external_id":"dep_0192a3b4-5c6d-7e8f-9a0b-1c2d3e4f5a6b","status":"paid"}}} ``` Este é o corpo do **vetor de teste normativo**: o `op_nero_hml` é um identificador fictício e os ids `evt_01J9ZQ5` e `dep_01J9ZQ3` são curtos de propósito. Em produção o `operation_id` é um UUID, os ids têm 32 caracteres hexadecimais e o `data.object` traz o depósito ou o saque completo. O formato real está em [Envelope](envelope.md) e [Catálogo de eventos](catalogo-de-eventos.md). #### Vetores normativos Os vetores de [`vetores-assinatura.json`](../exemplos/vetores-assinatura.json) são **públicos e de teste**: nunca use esses segredos fora de teste. Toda implementação precisa reproduzi-los byte a byte. Para testar a janela de tempo, fixe "agora" em `1790000300`. **Vetor 1: assinatura simples** - Segredo: `whsec_test_vetor_fedcba9876543210` - Mensagem assinada: `1790000300.` seguido do corpo cru mostrado acima. - `v1` = `4f82d5dac571d1a7b4dae9ef852c74cffcb6a4ce76f7f3209d685798e02a229b` - Resultado: **aceita**. **Vetor 2: rotação (dois `v1`)** ``` Dracma-Signature: t=1790000300,v1=4f82d5dac571d1a7b4dae9ef852c74cffcb6a4ce76f7f3209d685798e02a229b,v1=f146fd13a7702bb83873e1f2ff96947c854086637cc252e536b18f3dd6569920 ``` - O primeiro `v1` usa o segredo **novo** (`whsec_test_vetor_fedcba9876543210`). O segundo usa o **anterior** (`whsec_test_vetor_ANTERIOR`). - Um receptor que tem **só o segredo novo** aceita, porque o 1º `v1` bate. - Um receptor que ainda tem **só o segredo anterior** também aceita, porque o 2º `v1` bate. É isso que permite rotacionar sem perder nenhuma entrega. **Vetor 3: negativo (DEVE recusar)** - O corpo tem **um byte alterado** (`"object_version":4` no lugar de `3`), mas o header é a assinatura do vetor 1. - O HMAC correto desse corpo seria `47ab23d3…c77a6995`, e ele não bate com `4f82d5da…`. Resultado: **401**. - Um receptor que aceita esse vetor está verificando outra coisa que não o corpo cru (por exemplo, o JSON reserializado ou só alguns campos). Para conferir o vetor 1 no terminal (o `printf` não adiciona quebra de linha ao final): ```bash printf '%s' '1790000300.{"id":"evt_01J9ZQ5","type":"deposit.paid","created_at":"2026-09-26T15:00:00Z","operation_id":"op_nero_hml","object_id":"dep_01J9ZQ3","object_version":3,"data":{"object":{"id":"dep_01J9ZQ3","external_id":"dep_0192a3b4-5c6d-7e8f-9a0b-1c2d3e4f5a6b","status":"paid"}}}' \ | openssl dgst -sha256 -hmac whsec_test_vetor_fedcba9876543210 ``` A saída termina em `…02a229b`. Para conferir o vetor 2 com o segredo anterior, troque a chave por `whsec_test_vetor_ANTERIOR`: o resultado começa com `f146fd13`. #### Verificação em Python O trecho abaixo faz as três primeiras verificações da ordem do receptor (formato do header, janela e assinatura) e reproduz os três vetores. `corpo` são os bytes crus da requisição, antes de qualquer parse. ```python import hashlib, hmac, re, time CANONICO = re.compile(r"^(0|[1-9][0-9]{0,18})$") def verificar(segredos, ts_header, sig_header, corpo, agora=None): """Devolve None se válido, ou o motivo (responda 401). corpo = bytes crus.""" if not ts_header or not sig_header: return "headers ausentes" t, v1 = [], [] for parte in sig_header.split(","): chave, _, valor = parte.strip().partition("=") if chave == "t": t.append(valor) elif chave == "v1": v1.append(valor) # chaves desconhecidas: ignore (versões futuras) if len(t) != 1 or not v1 or not CANONICO.match(t[0]): return "Dracma-Signature malformado" if ts_header != t[0]: return "Dracma-Timestamp difere do t=" if abs((agora if agora is not None else time.time()) - int(t[0])) > 300: return "fora da janela de 300 s" ok = False for segredo in segredos: # 1 ou 2 segredos ativos esperado = hmac.new(segredo.encode(), t[0].encode() + b"." + corpo, hashlib.sha256).hexdigest() for recebido in v1: if hmac.compare_digest(esperado, recebido.lower()): ok = True # sem sair do laço no primeiro acerto return None if ok else "nenhum v1 confere" ``` Há clientes executáveis com `verificarWebhook` em Go, Node, PHP e shell em [Exemplos executáveis](../exemplos/README.md). ### Erros e casos de borda | Situação | Causa provável | O que fazer | |---|---|---| | Nenhum `v1` confere, mas o segredo parece certo | O corpo foi lido por um framework que já interpretou e reserializou o JSON, ou o proxy alterou os bytes (compressão, quebra de linha, `charset`). | Leia o corpo cru antes de qualquer middleware que interprete JSON. Registre o tamanho em bytes do corpo recebido e confira o hash do corpo antes e depois dos seus middlewares. | | Funciona com um segredo e falha logo depois de uma rotação | O receptor só conhece o segredo antigo ou só o novo e comparou **só o primeiro** `v1`. | Mantenha a lista de segredos ativos (1 ou 2) e teste todos os pares `v1` × segredo. | | `Dracma-Timestamp` diferente do `t=` | Um proxy reescreveu um dos headers, ou o seu código leu o header errado. | Os dois são sempre iguais na origem; reprove com 401. | | Falha só em reentregas | O relógio do servidor está fora de sincronia. A janela vale para o `t=` da tentativa, mas depende do seu relógio estar certo. | Sincronize o relógio por NTP. | | O header traz uma chave além de `t` e `v1` | Esquema de assinatura de versão futura. | Ignore a chave. Nunca falhe a verificação por causa dela. | | O vetor 3 foi aceito | Você verifica o JSON reserializado ou só alguns campos. | Verifique os bytes crus. | Uma resposta `401` ou `400` do seu receptor **não** faz o Dracma desistir: toda resposta fora de 2xx provoca nova tentativa por até 72 h. Veja [Retentativas, skipped e DLQ](retentativas-e-dlq.md#a-janela-de-72-horas). ### Veja também - [Envelope](envelope.md): campos do corpo. - [Receptor passo a passo](receptor.md): a ordem completa de verificação. - [Segredo e rotação](segredos-e-rotacao.md): formato do `whsec_`, rotação e vazamento. - [Exemplos executáveis](../exemplos/README.md): verificação em Go, Node, PHP e shell. --- # Arquivo: docs/webhooks/envelope.md ## Envelope > Para: Dev — Conferido em 2026-10-18. Todo webhook e todo item de `GET /events` têm o mesmo envelope: sete campos de topo e o objeto completo (depósito ou saque) em `data.object`. Esta página define cada campo, o formato dos ids e a relação entre `object_version` e `version`. ### O que é O envelope é o objeto `Evento` da [referência da API](../referencia/schemas.md). O Dracma serializa o corpo em JSON compacto, com os campos de topo sempre na ordem abaixo. Não dependa da ordem nem da formatação: leia o JSON por nome de campo e ignore campos que você não conhece. | Campo | Tipo | Nulo? | Exemplo | Descrição | |---|---|---|---|---| | `id` | string | nunca | `evt_5a9c1f0e7b3d4c28a6e1f4b07d92c385` | Id do evento: `evt_` seguido de 32 caracteres hexadecimais minúsculos. É **igual** ao header `Dracma-Event-Id` e é a chave de deduplicação. Um evento mantém o mesmo `id` em todas as tentativas e reentregas. | | `type` | string | nunca | `deposit.paid` | Tipo do evento: `.`, sem sufixo de versão. Lista em [Catálogo de eventos](catalogo-de-eventos.md). | | `created_at` | string | nunca | `2026-10-18T14:05:13Z` | Instante em que a transição aconteceu, em RFC 3339, sempre em UTC com `Z` e **sem frações de segundo** no webhook. | | `operation_id` | string | nunca | `0198b7c2-3d4e-7f50-8a61-92b3c4d5e6f7` | UUID da sua operação, em minúsculas. Um evento só chega à URL da operação a que pertence. | | `object_id` | string | nunca | `dep_0198b7c24d5e7a1b8c2d3e4f5a6b7c8d` | Id do objeto: `dep_` (depósito) ou `wdr_` (saque) seguido de 32 hexadecimais. É **igual** a `data.object.id`. | | `object_version` | inteiro | nunca | `3` | Versão do objeto na transição, maior ou igual a 1. É **igual** a `data.object.version`. | | `data.object` | objeto | nunca | veja abaixo | O depósito ou o saque completo, como estava no momento da transição. | Todos os campos são obrigatórios e nenhum deles vem nulo. Os ids `evt_`, `dep_` e `wdr_` são opacos: trate-os como texto, sem interpretar o conteúdo. ### Quando usar Use esta página para modelar o parser do seu receptor e a tabela de eventos já processados: - **Deduplicação:** guarde `id` (igual a `Dracma-Event-Id`) por no mínimo 7 dias. - **Fetch-back:** use `object_id` para consultar `GET /v1/deposits/{object_id}` ou `GET /v1/withdrawals/{object_id}`. O tipo de objeto vem do prefixo do `type` (`deposit.` ou `withdrawal.`). - **Ordenação:** compare `object_version` com a última versão aplicada para o mesmo `object_id`. ### Como funciona #### `data.object` O `data.object` é o **mesmo objeto** que a API devolve nas consultas: o `Deposito` em eventos `deposit.*` e o `Saque` em eventos `withdrawal.*`, com o `status` igual ao estado anunciado no `type`. Ele traz o objeto inteiro, não só o `id` e o `status`, e fica **congelado** no estado da transição: uma reentrega feita horas depois leva o objeto daquele momento, não o atual. Por isso o seu receptor nunca credita pelo corpo do webhook e sempre confirma o estado atual com uma consulta autenticada. Campos que mudam com o estado, por tipo de objeto: | Objeto | Campos que variam por estado | |---|---| | Depósito | `status`, `version`, `instructions` (preenchido a partir de `deposit.pending`), `paid_amount`, `payer` e `paid_at` (preenchidos em `deposit.paid` e `deposit.under_review`), `review_reasons` (preenchido em `deposit.under_review`), `failure_code` (preenchido em `deposit.failed`), `expires_at`, `updated_at`. | | Saque | `status`, `version`, `decision` (`mode`, `reasons`, `decided_by`, `decided_at`), `psp_reference` (preenchido em `withdrawal.paid`), `failure_code` (preenchido em `withdrawal.failed`), `paid_at`, `return` (preenchido em `withdrawal.returned`), `updated_at`. | O payload completo de cada evento está em [Catálogo de eventos](catalogo-de-eventos.md). As definições campo a campo estão em [Objetos e campos](../referencia/schemas.md). #### `object_version` e `version` `object_version` (no envelope) e `version` (dentro de `data.object`) são **o mesmo contador**: sobe em 1 a cada transição do objeto. Se o envelope diz `"object_version": 3`, o objeto traz `"version": 3`. O contador só cresce, então ele ordena os eventos de um mesmo objeto mesmo quando a entrega chega fora de ordem: ignore um evento cuja versão seja menor ou igual à última que você aplicou. O primeiro evento de um depósito é `deposit.pending`, com versão 2 (o estado interno inicial não vira evento). O primeiro evento visível de um saque depende da decisão: pode ser `withdrawal.under_review` ou `withdrawal.approved`, também com versão 2. As versões que você vê podem não ser contíguas: o estado inicial de um saque (`requested`), por exemplo, não gera evento público. #### Formato dos instantes - **No envelope**, `created_at` do webhook tem precisão de segundos (`2026-10-18T14:05:13Z`). - **Dentro de `data.object`**, os instantes do depósito têm precisão de segundos. Os do saque podem trazer **frações de segundo** (por exemplo `2026-10-18T15:00:09.482913Z`). - **Em `GET /events`**, o `created_at` do envelope de um saque também pode trazer frações. Faça o parse de RFC 3339 com e sem frações. Valores monetários vêm como objeto `{ "amount": "", "currency": "BRL" }`, com o valor inteiro na menor unidade da moeda. ### Exemplos Um evento `deposit.paid` completo, como chega ao seu receptor (formatado aqui para leitura; o corpo real é compacto). Os dados são fictícios. ```json { "id": "evt_e12d177892007213962d29c4b3b60580", "type": "deposit.paid", "created_at": "2026-10-18T14:05:13Z", "operation_id": "0198b7c2-3d4e-7f50-8a61-92b3c4d5e6f7", "object_id": "dep_0198b7c24d5e7a1b8c2d3e4f5a6b7c8d", "object_version": 3, "data": { "object": { "id": "dep_0198b7c24d5e7a1b8c2d3e4f5a6b7c8d", "external_id": "pedido-10482", "status": "paid", "version": 3, "amount": { "amount": "10000", "currency": "BRL" }, "paid_amount": { "amount": "10000", "currency": "BRL" }, "method": "pix", "player_ref": "jogador-7731", "payer": { "document": { "type": "CPF", "number": "12345678909", "country": "BR" }, "name": "Maria Exemplo", "matches_player": true }, "instructions": { "pix": { "copy_paste": "00020126580014br.gov.bcb.pix0136exemplo-chave-ficticia5204000053039865406100.005802BR5909EXEMPLO6009SAO PAULO62070503***6304ABCD", "expires_at": "2026-10-18T14:30:00Z" } }, "failure_code": null, "review_reasons": [], "created_at": "2026-10-18T14:00:00Z", "updated_at": "2026-10-18T14:05:13Z", "paid_at": "2026-10-18T14:05:12Z", "expires_at": "2026-10-18T14:30:00Z" } } } ``` Repare que `object_id` repete `data.object.id` e que `object_version` repete `data.object.version`. ### Erros e casos de borda - **Campo desconhecido:** ignore. O Dracma só acrescenta campos; nunca falhe a leitura por causa de um. - **`type` desconhecido:** responda 200, grave o `id` para a deduplicação e não mova dinheiro. Veja [Receptor passo a passo](receptor.md). - **`status` desconhecido em `data.object`:** trate o objeto como não terminal e confirme depois. - **Id curto nos exemplos antigos:** a referência da API e os vetores de assinatura usam ids curtos como `evt_01J9ZQ5`. É só um exemplo; o formato real tem 32 caracteres hexadecimais depois do prefixo. Não valide o comprimento de forma rígida: use apenas o prefixo e o conjunto de caracteres. - **`operation_id` no vetor de teste:** o valor `op_nero_hml` dos vetores é fictício e não tem o formato de UUID da operação real. - **Objeto fora de ordem:** um evento com `object_version` menor que o já aplicado não é erro; responda 200 sem efeito. ### Veja também - [Catálogo de eventos](catalogo-de-eventos.md): payload completo de cada evento. - [Requisição e assinatura](requisicao-e-assinatura.md): headers e assinatura. - [Recuperação com `GET /events`](recuperacao-get-events.md): o mesmo envelope, em lista. - [Objetos e campos](../referencia/schemas.md): definição campo a campo. --- # Arquivo: docs/webhooks/catalogo-de-eventos.md ## Catálogo de eventos > Para: Dev — Conferido em 2026-10-18. O Dracma entrega 17 tipos de evento: 9 de depósito e 8 de saque. Esta página lista cada um com o momento em que dispara, o que muda no objeto e o payload completo. O que decide o seu efeito financeiro é sempre o estado devolvido pela consulta à API, não o `type` do evento. ### O que é O catálogo público é o conjunto de eventos `deposit.` e `withdrawal.`. O `type` é o objeto mais o estado alcançado, sem sufixo de versão. Cada evento corresponde a **uma transição** de estado e leva o objeto completo naquele estado ([Envelope](envelope.md)). | Evento | Quando dispara | Em produção hoje | |---|---|---| | `deposit.pending` | Instruções de pagamento prontas. Primeiro evento do depósito. | Sim | | `deposit.under_review` | Pagamento recebido e retido para revisão. | Sim | | `deposit.paid` | Pagamento confirmado. | Sim | | `deposit.expired` | Prazo de pagamento esgotado. | Sim | | `deposit.cancelled` | Cancelamento por você, em `pending`. | Sim | | `deposit.failed` | Cobrança não criada ou recusada. | Sim | | `deposit.returned` | Depósito em revisão devolvido: não foi aceito e não é creditado. | Sim | | `deposit.refunded` | Estorno de depósito pago. | Não: só no sandbox | | `deposit.chargeback` | Contestação de depósito pago. | Não: só no sandbox | | `withdrawal.under_review` | Saque retido para revisão manual. | Sim | | `withdrawal.approved` | Saque aprovado. | Sim | | `withdrawal.processing` | Pagamento enviado ao PSP, aguardando confirmação. | Sim | | `withdrawal.paid` | Saque pago. | Sim | | `withdrawal.rejected` | Saque recusado. | Sim | | `withdrawal.failed` | Pagamento falhou depois do envio. | Sim | | `withdrawal.cancelled` | Saque cancelado antes do envio. | Sim | | `withdrawal.returned` | Valor devolvido pelo banco depois do `paid`. | Sim | !!! warning "Não disponível" `deposit.refunded` e `deposit.chargeback` fazem parte do catálogo e já podem ser assinados, mas em produção o Dracma não os emite hoje: eles só ocorrem no sandbox, pelas ações `refund` e `chargeback`. Implemente o tratamento agora, porque o seu código precisa sobreviver a eles; não conte com eles para conciliar dinheiro em produção. ### Quando usar Use o catálogo para: - Decidir quais eventos assinar (veja o filtro de tipos em [Retentativas, skipped e DLQ](retentativas-e-dlq.md#skipped-o-evento-foi-registrado-e-nada-foi-enviado) e [Webhooks no painel](../painel/webhooks.md)). - Escrever a tabela "evento → ação" do receptor. - Montar os testes do receptor com payloads realistas, na seção [Exemplos](#exemplos). ### Como funciona #### O primeiro evento de cada objeto - **Depósito:** o primeiro evento é `deposit.pending`, com `object_version` 2. Não existe `deposit.created`: a criação responde pela própria chamada `POST /v1/deposits`. - **Saque:** não existe evento público `withdrawal.requested`. O primeiro evento é o de decisão (`withdrawal.under_review` ou `withdrawal.approved`, e, se as regras recusam de imediato, `withdrawal.rejected`). #### Transições que geram evento ```mermaid stateDiagram-v2 [*] --> pending [*] --> failed pending --> paid pending --> under_review pending --> expired pending --> cancelled pending --> failed under_review --> paid under_review --> returned paid --> refunded paid --> chargeback ``` ```mermaid stateDiagram-v2 [*] --> under_review [*] --> approved [*] --> rejected [*] --> cancelled under_review --> approved under_review --> rejected under_review --> cancelled approved --> processing approved --> cancelled processing --> paid processing --> failed paid --> returned ``` #### O que fazer em cada evento O que manda é **sempre o estado devolvido pela consulta à API**, não o `type` do evento. O `type` só diz qual objeto consultar. Os eventos podem chegar fora de ordem e podem ser forjados; veja [Receptor passo a passo](receptor.md#estado-devolvido-acao). **Depósitos** (detalhes em [Ciclo do depósito](../dinheiro/ciclo-do-deposito.md)): | Evento | Estado na consulta | Ação do operador | |---|---|---| | `deposit.pending` | `pending` | Nenhum dinheiro se move. Exiba o QR Code ou o copia-e-cola ao jogador. | | `deposit.under_review` | `under_review` | Nenhum dinheiro se move. Os motivos estão em `review_reasons`. Avise o jogador de que o pagamento está em análise. | | `deposit.paid` | `paid` | **Credita o jogador** pelo valor que **você registrou** na criação, uma única vez. | | `deposit.expired`, `deposit.cancelled`, `deposit.failed` | idem | Terminal. Nenhum crédito. | | `deposit.returned` | `returned` | Terminal. Nenhum crédito. | | `deposit.refunded`, `deposit.chargeback` | idem | **Débito** de um depósito já creditado. A política é sua. Recomendação: não debite automaticamente; abra uma decisão manual e dispare um alerta. | **Saques** (detalhes em [Ciclo do saque](../dinheiro/ciclo-do-saque.md)). Você reservou (debitou) o valor antes de chamar `POST /v1/withdrawals`: | Evento | Estado na consulta | Ação do operador | |---|---|---| | `withdrawal.under_review`, `withdrawal.approved`, `withdrawal.processing` | idem | Nenhum dinheiro se move. A reserva continua. | | `withdrawal.paid` | `paid` | Finaliza: a reserva vira saída definitiva. `psp_reference` identifica o pagamento. | | `withdrawal.rejected`, `withdrawal.failed`, `withdrawal.cancelled` | idem | **Devolve a reserva** ao jogador, uma única vez. | | `withdrawal.returned` | `returned` | O banco devolveu o valor depois do `paid`, e o dinheiro voltou ao caixa da operação. A política é sua. Recomendação: não recredite o jogador automaticamente; abra uma decisão manual e dispare um alerta. | #### Eventos reservados e tipos futuros !!! warning "Não disponível" `rules.updated` aparece em listas antigas de eventos, mas **nunca é entregue** e **não pode ser assinado**: o Dracma não emite esse evento. Para saber se as regras mudaram, consulte `GET /v1/rules` quando precisar. !!! note "Reservado no contrato" O contrato reserva tipos de versões mais novas, que pertencem a métodos ainda não disponíveis (transferência bancária, SWIFT, cripto, cartão) ou a regras desligadas: `deposit.pending_compliance`, `deposit.frozen`, `deposit.paid_partial`, `withdrawal.pending_compliance`, `withdrawal.sent`, `withdrawal.in_transit`, `withdrawal.recall_requested`, `withdrawal.recall_rejected`, `withdrawal.recalled` e `receipt.unmatched`, `receipt.matched`, `receipt.returned`, `receipt.frozen`. Nenhum deles tem gatilho hoje. O seu código precisa sobreviver a eles mesmo assim: o catálogo só cresce. - **`type` desconhecido** ou `status` desconhecido na consulta: responda **200**, grave o `id` do evento para a deduplicação, **não credite, não debite e não devolva a reserva**, e trate o objeto como não terminal. Confirme de novo depois, pela consulta e por `GET /events`. - **Campo desconhecido no JSON:** ignore. Nunca falhe a leitura por causa dele. - Dinheiro só se move em estado que você conhece e cuja regra está escrita no seu código. ### Exemplos Cada bloco abaixo é o corpo completo de um webhook (formatado para leitura; o corpo real é compacto). Os dados são fictícios e consistentes entre os blocos: o mesmo depósito percorre os estados de depósito e o mesmo saque percorre os de saque, exceto nos desfechos alternativos (por exemplo, `deposit.expired` e `deposit.paid` são desfechos excludentes do mesmo `pending`). Os campos de saque que o contrato reserva para versões futuras (`rail`, `fees`, `fx` e semelhantes) vêm sempre `null` ou vazios. #### `deposit.pending` O depósito foi aceito e as instruções de pagamento estão prontas. É o **primeiro** evento de um depósito (não existe `deposit.created`). - `instructions.pix.copy_paste` traz o código copia-e-cola do PIX e `instructions.pix.expires_at` o limite para pagar. - `paid_amount`, `payer` e `paid_at` ainda são `null`; `review_reasons` é `[]`. - `version` é 2. ```json { "id": "evt_686599cbb91280cbb34334f16f643047", "type": "deposit.pending", "created_at": "2026-10-18T14:00:02Z", "operation_id": "0198b7c2-3d4e-7f50-8a61-92b3c4d5e6f7", "object_id": "dep_0198b7c24d5e7a1b8c2d3e4f5a6b7c8d", "object_version": 2, "data": { "object": { "id": "dep_0198b7c24d5e7a1b8c2d3e4f5a6b7c8d", "external_id": "pedido-10482", "status": "pending", "version": 2, "amount": { "amount": "10000", "currency": "BRL" }, "paid_amount": null, "method": "pix", "player_ref": "jogador-7731", "payer": null, "instructions": { "pix": { "copy_paste": "00020126580014br.gov.bcb.pix0136exemplo-chave-ficticia5204000053039865406100.005802BR5909EXEMPLO6009SAO PAULO62070503***6304ABCD", "expires_at": "2026-10-18T14:30:00Z" } }, "failure_code": null, "review_reasons": [], "created_at": "2026-10-18T14:00:00Z", "updated_at": "2026-10-18T14:00:02Z", "paid_at": null, "expires_at": "2026-10-18T14:30:00Z" } } } ``` #### `deposit.under_review` O pagamento chegou, mas ficou retido para revisão por um ou mais motivos em `review_reasons` (por exemplo `PAYER_MISMATCH`, `AMOUNT_MISMATCH`, `PAID_AFTER_EXPIRY`; a lista é aberta). - `paid_amount`, `payer` e `paid_at` já estão preenchidos; `payer.matches_player` mostra se o pagador é o jogador. - O depósito ainda **não creditou ninguém**. O desfecho vem em `deposit.paid` ou `deposit.returned`, depois da decisão de revisão no painel. ```json { "id": "evt_2b71e0124e8cbbe5451debd56cbc66b7", "type": "deposit.under_review", "created_at": "2026-10-18T14:05:13Z", "operation_id": "0198b7c2-3d4e-7f50-8a61-92b3c4d5e6f7", "object_id": "dep_0198b7c24d5e7a1b8c2d3e4f5a6b7c8d", "object_version": 3, "data": { "object": { "id": "dep_0198b7c24d5e7a1b8c2d3e4f5a6b7c8d", "external_id": "pedido-10482", "status": "under_review", "version": 3, "amount": { "amount": "10000", "currency": "BRL" }, "paid_amount": { "amount": "10000", "currency": "BRL" }, "method": "pix", "player_ref": "jogador-7731", "payer": { "document": { "type": "CPF", "number": "98765432100", "country": "BR" }, "name": "Joao Exemplo", "matches_player": false }, "instructions": { "pix": { "copy_paste": "00020126580014br.gov.bcb.pix0136exemplo-chave-ficticia5204000053039865406100.005802BR5909EXEMPLO6009SAO PAULO62070503***6304ABCD", "expires_at": "2026-10-18T14:30:00Z" } }, "failure_code": null, "review_reasons": [ "PAYER_MISMATCH" ], "created_at": "2026-10-18T14:00:00Z", "updated_at": "2026-10-18T14:05:13Z", "paid_at": "2026-10-18T14:05:12Z", "expires_at": "2026-10-18T14:30:00Z" } } } ``` #### `deposit.paid` O pagamento foi confirmado e liberado. É o único estado de depósito que credita o jogador. - `paid_amount`, `payer` e `paid_at` estão preenchidos. - Também é o evento do depósito que estava em revisão e foi aceito na revisão do painel. ```json { "id": "evt_e12d177892007213962d29c4b3b60580", "type": "deposit.paid", "created_at": "2026-10-18T14:05:13Z", "operation_id": "0198b7c2-3d4e-7f50-8a61-92b3c4d5e6f7", "object_id": "dep_0198b7c24d5e7a1b8c2d3e4f5a6b7c8d", "object_version": 3, "data": { "object": { "id": "dep_0198b7c24d5e7a1b8c2d3e4f5a6b7c8d", "external_id": "pedido-10482", "status": "paid", "version": 3, "amount": { "amount": "10000", "currency": "BRL" }, "paid_amount": { "amount": "10000", "currency": "BRL" }, "method": "pix", "player_ref": "jogador-7731", "payer": { "document": { "type": "CPF", "number": "12345678909", "country": "BR" }, "name": "Maria Exemplo", "matches_player": true }, "instructions": { "pix": { "copy_paste": "00020126580014br.gov.bcb.pix0136exemplo-chave-ficticia5204000053039865406100.005802BR5909EXEMPLO6009SAO PAULO62070503***6304ABCD", "expires_at": "2026-10-18T14:30:00Z" } }, "failure_code": null, "review_reasons": [], "created_at": "2026-10-18T14:00:00Z", "updated_at": "2026-10-18T14:05:13Z", "paid_at": "2026-10-18T14:05:12Z", "expires_at": "2026-10-18T14:30:00Z" } } } ``` #### `deposit.expired` O prazo de pagamento passou sem pagamento. Estado terminal. - `paid_amount`, `payer` e `paid_at` seguem `null`; as `instructions` continuam no objeto, mas não valem mais. ```json { "id": "evt_778446c795b6d20f8acb5355a32a953e", "type": "deposit.expired", "created_at": "2026-10-18T14:30:05Z", "operation_id": "0198b7c2-3d4e-7f50-8a61-92b3c4d5e6f7", "object_id": "dep_0198b7c24d5e7a1b8c2d3e4f5a6b7c8d", "object_version": 3, "data": { "object": { "id": "dep_0198b7c24d5e7a1b8c2d3e4f5a6b7c8d", "external_id": "pedido-10482", "status": "expired", "version": 3, "amount": { "amount": "10000", "currency": "BRL" }, "paid_amount": null, "method": "pix", "player_ref": "jogador-7731", "payer": null, "instructions": { "pix": { "copy_paste": "00020126580014br.gov.bcb.pix0136exemplo-chave-ficticia5204000053039865406100.005802BR5909EXEMPLO6009SAO PAULO62070503***6304ABCD", "expires_at": "2026-10-18T14:30:00Z" } }, "failure_code": null, "review_reasons": [], "created_at": "2026-10-18T14:00:00Z", "updated_at": "2026-10-18T14:30:05Z", "paid_at": null, "expires_at": "2026-10-18T14:30:00Z" } } } ``` #### `deposit.cancelled` O depósito foi cancelado por `POST /v1/deposits/{id}/cancel` enquanto estava `pending`. Estado terminal. - O cancelamento só é aceito em `pending`. ```json { "id": "evt_838f541b2f29b73a96d98632accec428", "type": "deposit.cancelled", "created_at": "2026-10-18T14:10:00Z", "operation_id": "0198b7c2-3d4e-7f50-8a61-92b3c4d5e6f7", "object_id": "dep_0198b7c24d5e7a1b8c2d3e4f5a6b7c8d", "object_version": 3, "data": { "object": { "id": "dep_0198b7c24d5e7a1b8c2d3e4f5a6b7c8d", "external_id": "pedido-10482", "status": "cancelled", "version": 3, "amount": { "amount": "10000", "currency": "BRL" }, "paid_amount": null, "method": "pix", "player_ref": "jogador-7731", "payer": null, "instructions": { "pix": { "copy_paste": "00020126580014br.gov.bcb.pix0136exemplo-chave-ficticia5204000053039865406100.005802BR5909EXEMPLO6009SAO PAULO62070503***6304ABCD", "expires_at": "2026-10-18T14:30:00Z" } }, "failure_code": null, "review_reasons": [], "created_at": "2026-10-18T14:00:00Z", "updated_at": "2026-10-18T14:10:00Z", "paid_at": null, "expires_at": "2026-10-18T14:30:00Z" } } } ``` #### `deposit.failed` A cobrança não foi criada ou foi recusada. Estado terminal. - `failure_code` traz o motivo como texto; trate-o como opaco. - Quando a falha acontece antes de o depósito ficar `pending`, `instructions` é `null` e a `version` é 2. ```json { "id": "evt_7ea95f4af5ebdcc1371cfc51e86f7f4f", "type": "deposit.failed", "created_at": "2026-10-18T14:00:02Z", "operation_id": "0198b7c2-3d4e-7f50-8a61-92b3c4d5e6f7", "object_id": "dep_0198b7c24d5e7a1b8c2d3e4f5a6b7c8d", "object_version": 2, "data": { "object": { "id": "dep_0198b7c24d5e7a1b8c2d3e4f5a6b7c8d", "external_id": "pedido-10482", "status": "failed", "version": 2, "amount": { "amount": "10000", "currency": "BRL" }, "paid_amount": null, "method": "pix", "player_ref": "jogador-7731", "payer": null, "instructions": null, "failure_code": "psp_rejected", "review_reasons": [], "created_at": "2026-10-18T14:00:00Z", "updated_at": "2026-10-18T14:00:02Z", "paid_at": null, "expires_at": "2026-10-18T14:30:00Z" } } } ``` #### `deposit.returned` O depósito estava em revisão e foi devolvido (não aceito). Estado terminal; o jogador **não é creditado**. - `review_reasons` mantém os motivos que levaram à revisão. ```json { "id": "evt_ecc47d81a80f12aaef9d7f3dbbacafc6", "type": "deposit.returned", "created_at": "2026-10-18T16:20:00Z", "operation_id": "0198b7c2-3d4e-7f50-8a61-92b3c4d5e6f7", "object_id": "dep_0198b7c24d5e7a1b8c2d3e4f5a6b7c8d", "object_version": 4, "data": { "object": { "id": "dep_0198b7c24d5e7a1b8c2d3e4f5a6b7c8d", "external_id": "pedido-10482", "status": "returned", "version": 4, "amount": { "amount": "10000", "currency": "BRL" }, "paid_amount": { "amount": "10000", "currency": "BRL" }, "method": "pix", "player_ref": "jogador-7731", "payer": { "document": { "type": "CPF", "number": "98765432100", "country": "BR" }, "name": "Joao Exemplo", "matches_player": false }, "instructions": { "pix": { "copy_paste": "00020126580014br.gov.bcb.pix0136exemplo-chave-ficticia5204000053039865406100.005802BR5909EXEMPLO6009SAO PAULO62070503***6304ABCD", "expires_at": "2026-10-18T14:30:00Z" } }, "failure_code": null, "review_reasons": [ "PAYER_MISMATCH" ], "created_at": "2026-10-18T14:00:00Z", "updated_at": "2026-10-18T16:20:00Z", "paid_at": "2026-10-18T14:05:12Z", "expires_at": "2026-10-18T14:30:00Z" } } } ``` #### `deposit.refunded` Um depósito já `paid` foi estornado. Estado terminal. - Em produção este evento **não é emitido hoje**; ele só ocorre no sandbox, pela ação `refund`. ```json { "id": "evt_8650c7d795cd73a5699cfb0aa9f620c2", "type": "deposit.refunded", "created_at": "2026-10-18T18:00:00Z", "operation_id": "0198b7c2-3d4e-7f50-8a61-92b3c4d5e6f7", "object_id": "dep_0198b7c24d5e7a1b8c2d3e4f5a6b7c8d", "object_version": 4, "data": { "object": { "id": "dep_0198b7c24d5e7a1b8c2d3e4f5a6b7c8d", "external_id": "pedido-10482", "status": "refunded", "version": 4, "amount": { "amount": "10000", "currency": "BRL" }, "paid_amount": { "amount": "10000", "currency": "BRL" }, "method": "pix", "player_ref": "jogador-7731", "payer": { "document": { "type": "CPF", "number": "12345678909", "country": "BR" }, "name": "Maria Exemplo", "matches_player": true }, "instructions": { "pix": { "copy_paste": "00020126580014br.gov.bcb.pix0136exemplo-chave-ficticia5204000053039865406100.005802BR5909EXEMPLO6009SAO PAULO62070503***6304ABCD", "expires_at": "2026-10-18T14:30:00Z" } }, "failure_code": null, "review_reasons": [], "created_at": "2026-10-18T14:00:00Z", "updated_at": "2026-10-18T18:00:00Z", "paid_at": "2026-10-18T14:05:12Z", "expires_at": "2026-10-18T14:30:00Z" } } } ``` #### `deposit.chargeback` Um depósito já `paid` sofreu contestação (chargeback/MED). Estado terminal. - Em produção este evento **não é emitido hoje**; ele só ocorre no sandbox, pela ação `chargeback`. ```json { "id": "evt_bd9b8dee7775b625eb1708bc52e2d8c4", "type": "deposit.chargeback", "created_at": "2026-10-18T19:00:00Z", "operation_id": "0198b7c2-3d4e-7f50-8a61-92b3c4d5e6f7", "object_id": "dep_0198b7c24d5e7a1b8c2d3e4f5a6b7c8d", "object_version": 4, "data": { "object": { "id": "dep_0198b7c24d5e7a1b8c2d3e4f5a6b7c8d", "external_id": "pedido-10482", "status": "chargeback", "version": 4, "amount": { "amount": "10000", "currency": "BRL" }, "paid_amount": { "amount": "10000", "currency": "BRL" }, "method": "pix", "player_ref": "jogador-7731", "payer": { "document": { "type": "CPF", "number": "12345678909", "country": "BR" }, "name": "Maria Exemplo", "matches_player": true }, "instructions": { "pix": { "copy_paste": "00020126580014br.gov.bcb.pix0136exemplo-chave-ficticia5204000053039865406100.005802BR5909EXEMPLO6009SAO PAULO62070503***6304ABCD", "expires_at": "2026-10-18T14:30:00Z" } }, "failure_code": null, "review_reasons": [], "created_at": "2026-10-18T14:00:00Z", "updated_at": "2026-10-18T19:00:00Z", "paid_at": "2026-10-18T14:05:12Z", "expires_at": "2026-10-18T14:30:00Z" } } } ``` #### `withdrawal.under_review` O saque ficou retido para revisão manual. `decision.mode` é `manual` e `decision.reasons` traz os códigos da regra que exigiu revisão. - Ainda não há `decided_by` nem `decided_at`. O desfecho vem em `withdrawal.approved`, `withdrawal.rejected` ou `withdrawal.cancelled`. ```json { "id": "evt_29ad1449656a8e1d6e85c8984ea98109", "type": "withdrawal.under_review", "created_at": "2026-10-18T15:00:01Z", "operation_id": "0198b7c2-3d4e-7f50-8a61-92b3c4d5e6f7", "object_id": "wdr_0198b7c3a1b27c4d8e5f60718293a4b5", "object_version": 2, "data": { "object": { "id": "wdr_0198b7c3a1b27c4d8e5f60718293a4b5", "external_id": "saque-55120", "status": "under_review", "version": 2, "amount": { "amount": "25000", "currency": "BRL" }, "fee": null, "method": "pix", "player_ref": "jogador-7731", "decision": { "mode": "manual", "reasons": [ "FIRST_WITHDRAWAL" ], "ruleset_version": 3, "decided_by": [], "decided_at": null }, "psp_reference": null, "failure_code": null, "created_at": "2026-10-18T15:00:00.482913Z", "updated_at": "2026-10-18T15:00:01.482913Z", "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 } } } ``` #### `withdrawal.approved` O saque foi aprovado (automaticamente ou depois da revisão) e segue para pagamento. - Neste exemplo a aprovação foi automática: `decision.mode` é `automatic` e `decision.reasons` é `[]`. Depois de uma revisão, `mode` é `manual` e `decided_by` lista os revisores (ids opacos). ```json { "id": "evt_3985aee553b478cd940b64cd894b5c9c", "type": "withdrawal.approved", "created_at": "2026-10-18T15:00:01Z", "operation_id": "0198b7c2-3d4e-7f50-8a61-92b3c4d5e6f7", "object_id": "wdr_0198b7c3a1b27c4d8e5f60718293a4b5", "object_version": 2, "data": { "object": { "id": "wdr_0198b7c3a1b27c4d8e5f60718293a4b5", "external_id": "saque-55120", "status": "approved", "version": 2, "amount": { "amount": "25000", "currency": "BRL" }, "fee": null, "method": "pix", "player_ref": "jogador-7731", "decision": { "mode": "automatic", "reasons": [], "ruleset_version": 3, "decided_by": [], "decided_at": "2026-10-18T15:00:01.482913Z" }, "psp_reference": null, "failure_code": null, "created_at": "2026-10-18T15:00:00.482913Z", "updated_at": "2026-10-18T15:00:01.482913Z", "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 } } } ``` #### `withdrawal.processing` O pagamento foi enviado ao PSP e aguarda a confirmação. Esse estado pode durar. - Nenhum dinheiro se move no seu livro: a reserva continua. ```json { "id": "evt_0a55546ba2d5971d5d2b40b53d8e41a5", "type": "withdrawal.processing", "created_at": "2026-10-18T15:00:05Z", "operation_id": "0198b7c2-3d4e-7f50-8a61-92b3c4d5e6f7", "object_id": "wdr_0198b7c3a1b27c4d8e5f60718293a4b5", "object_version": 3, "data": { "object": { "id": "wdr_0198b7c3a1b27c4d8e5f60718293a4b5", "external_id": "saque-55120", "status": "processing", "version": 3, "amount": { "amount": "25000", "currency": "BRL" }, "fee": null, "method": "pix", "player_ref": "jogador-7731", "decision": { "mode": "automatic", "reasons": [], "ruleset_version": 3, "decided_by": [], "decided_at": "2026-10-18T15:00:01.482913Z" }, "psp_reference": null, "failure_code": null, "created_at": "2026-10-18T15:00:00.482913Z", "updated_at": "2026-10-18T15:00:05.482913Z", "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 } } } ``` #### `withdrawal.paid` O saque foi pago. - `psp_reference` traz o identificador do pagamento no PSP (o E2E do PIX, quando informado) e `paid_at` o instante do pagamento. ```json { "id": "evt_f7530e95349e7188505699cbb57808c7", "type": "withdrawal.paid", "created_at": "2026-10-18T15:00:09Z", "operation_id": "0198b7c2-3d4e-7f50-8a61-92b3c4d5e6f7", "object_id": "wdr_0198b7c3a1b27c4d8e5f60718293a4b5", "object_version": 4, "data": { "object": { "id": "wdr_0198b7c3a1b27c4d8e5f60718293a4b5", "external_id": "saque-55120", "status": "paid", "version": 4, "amount": { "amount": "25000", "currency": "BRL" }, "fee": null, "method": "pix", "player_ref": "jogador-7731", "decision": { "mode": "automatic", "reasons": [], "ruleset_version": 3, "decided_by": [], "decided_at": "2026-10-18T15:00:01.482913Z" }, "psp_reference": "E00000000202610181500a1b2c3d4e5f", "failure_code": null, "created_at": "2026-10-18T15:00:00.482913Z", "updated_at": "2026-10-18T15:00:09.482913Z", "paid_at": "2026-10-18T15:00:09.482913Z", "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 } } } ``` #### `withdrawal.rejected` O saque foi recusado pelas regras ou pela revisão. Estado terminal; a reserva volta ao jogador. - Neste exemplo a recusa foi manual: `decision.mode` é `manual` e `decided_by` lista o revisor. ```json { "id": "evt_ddfeaba5f991ca15e30c0acab6a3320c", "type": "withdrawal.rejected", "created_at": "2026-10-18T15:20:00Z", "operation_id": "0198b7c2-3d4e-7f50-8a61-92b3c4d5e6f7", "object_id": "wdr_0198b7c3a1b27c4d8e5f60718293a4b5", "object_version": 3, "data": { "object": { "id": "wdr_0198b7c3a1b27c4d8e5f60718293a4b5", "external_id": "saque-55120", "status": "rejected", "version": 3, "amount": { "amount": "25000", "currency": "BRL" }, "fee": null, "method": "pix", "player_ref": "jogador-7731", "decision": { "mode": "manual", "reasons": [ "FIRST_WITHDRAWAL" ], "ruleset_version": 3, "decided_by": [ "0198b7c1-9d2e-7a40-b1c2-3d4e5f607182" ], "decided_at": "2026-10-18T15:20:00.482913Z" }, "psp_reference": null, "failure_code": null, "created_at": "2026-10-18T15:00:00.482913Z", "updated_at": "2026-10-18T15:20:00.482913Z", "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 } } } ``` #### `withdrawal.failed` O pagamento falhou depois do envio ao PSP. Estado terminal; a reserva volta ao jogador. - `failure_code` traz o motivo como texto; trate-o como opaco. ```json { "id": "evt_e789a50d09bedcb67ac6efbb41b4be81", "type": "withdrawal.failed", "created_at": "2026-10-18T15:00:09Z", "operation_id": "0198b7c2-3d4e-7f50-8a61-92b3c4d5e6f7", "object_id": "wdr_0198b7c3a1b27c4d8e5f60718293a4b5", "object_version": 4, "data": { "object": { "id": "wdr_0198b7c3a1b27c4d8e5f60718293a4b5", "external_id": "saque-55120", "status": "failed", "version": 4, "amount": { "amount": "25000", "currency": "BRL" }, "fee": null, "method": "pix", "player_ref": "jogador-7731", "decision": { "mode": "automatic", "reasons": [], "ruleset_version": 3, "decided_by": [], "decided_at": "2026-10-18T15:00:01.482913Z" }, "psp_reference": null, "failure_code": "psp_failed", "created_at": "2026-10-18T15:00:00.482913Z", "updated_at": "2026-10-18T15:00:09.482913Z", "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 } } } ``` #### `withdrawal.cancelled` O saque foi cancelado antes de ser enviado ao PSP (por `POST /v1/withdrawals/{id}/cancel` ou automaticamente, por exemplo quando o jogador entra numa lista de bloqueio antes do envio). Estado terminal; a reserva volta ao jogador. - O cancelamento só é aceito antes de `processing`. ```json { "id": "evt_5378edae6b397dfb7406e4a94e1bb93e", "type": "withdrawal.cancelled", "created_at": "2026-10-18T15:10:00Z", "operation_id": "0198b7c2-3d4e-7f50-8a61-92b3c4d5e6f7", "object_id": "wdr_0198b7c3a1b27c4d8e5f60718293a4b5", "object_version": 3, "data": { "object": { "id": "wdr_0198b7c3a1b27c4d8e5f60718293a4b5", "external_id": "saque-55120", "status": "cancelled", "version": 3, "amount": { "amount": "25000", "currency": "BRL" }, "fee": null, "method": "pix", "player_ref": "jogador-7731", "decision": { "mode": "manual", "reasons": [ "FIRST_WITHDRAWAL" ], "ruleset_version": 3, "decided_by": [], "decided_at": null }, "psp_reference": null, "failure_code": null, "created_at": "2026-10-18T15:00:00.482913Z", "updated_at": "2026-10-18T15:10:00.482913Z", "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 } } } ``` #### `withdrawal.returned` O banco devolveu o valor depois de o saque estar `paid`. Estado terminal. - `return` traz `reason_code` (código curto informado na devolução), `returned_amount` e `returned_at`. ```json { "id": "evt_930eb572efc340d1aa4b69f117dc41e2", "type": "withdrawal.returned", "created_at": "2026-10-19T09:30:00Z", "operation_id": "0198b7c2-3d4e-7f50-8a61-92b3c4d5e6f7", "object_id": "wdr_0198b7c3a1b27c4d8e5f60718293a4b5", "object_version": 5, "data": { "object": { "id": "wdr_0198b7c3a1b27c4d8e5f60718293a4b5", "external_id": "saque-55120", "status": "returned", "version": 5, "amount": { "amount": "25000", "currency": "BRL" }, "fee": null, "method": "pix", "player_ref": "jogador-7731", "decision": { "mode": "automatic", "reasons": [], "ruleset_version": 3, "decided_by": [], "decided_at": "2026-10-18T15:00:01.482913Z" }, "psp_reference": "E00000000202610181500a1b2c3d4e5f", "failure_code": null, "created_at": "2026-10-18T15:00:00.482913Z", "updated_at": "2026-10-19T09:30:00.482913Z", "paid_at": "2026-10-18T15:00:09.482913Z", "rail": null, "fees": [], "charge_bearer": null, "fx": null, "purpose": null, "compliance": null, "expected_settlement_date": null, "sent_at": null, "tracking": null, "return": { "reason_code": "MD06", "returned_amount": { "amount": "25000", "currency": "BRL" }, "returned_at": "2026-10-19T09:30:00.482913Z" }, "recall": null } } } ``` ### Erros e casos de borda - **Evento não assinado:** se a sua assinatura de eventos não inclui o tipo, o Dracma **não envia** aquele webhook (a entrega fica `skipped`), mas o evento continua disponível em `GET /events`. Quem assina só `deposit.paid` não vê `deposit.under_review` nem `deposit.expired` por webhook. O seu job de recuperação precisa tratar todos os tipos. Veja [Recuperação com `GET /events`](recuperacao-get-events.md). - **Transições rápidas:** cada transição gera o seu próprio webhook; o Dracma não junta transições. Mesmo assim, eventos distintos podem chegar em qualquer ordem, e o `data.object` de cada um é o estado daquela transição. Você pode ver `deposit.paid` antes de `deposit.pending`. Aplique pelo estado da consulta e por `object_version`. - **Pagamento depois da expiração:** se o dinheiro chegar depois de o depósito já estar `expired`, no código atual o depósito continua `expired` e **não há novo evento**; o Dracma sinaliza o caso internamente para tratamento. Se o seu jogador diz que pagou, consulte o depósito e abra um chamado com o `id` do depósito. Veja [Problemas comuns](troubleshooting.md). - **`withdrawal.returned` em produção:** o evento ocorre quando o banco devolve um saque já pago, e `return.reason_code` traz o código curto informado na devolução. No sandbox, a ação `return` simula o mesmo evento com um código de simulação. - **Estados internos:** estados que não estão nesta lista (como o `requested` do saque) não geram webhook. - **Lista que cresce:** o catálogo é aditivo. Um tipo novo é sempre opcional para você tratar, e um tipo desconhecido nunca é motivo para responder erro. ### Veja também - [Envelope](envelope.md): campos de topo e `object_version`. - [Receptor passo a passo](receptor.md): fetch-back e deduplicação. - [Ciclo do depósito](../dinheiro/ciclo-do-deposito.md) e [Ciclo do saque](../dinheiro/ciclo-do-saque.md): o que cada estado significa para o dinheiro. - [Receitas por caminho](../sandbox/receitas.md): quais ações do sandbox geram cada evento. --- # Arquivo: docs/webhooks/receptor.md ## Receptor passo a passo > Para: Dev — Conferido em 2026-10-18. O receptor é o seu endpoint de webhook. Ele precisa verificar a assinatura, deduplicar, confirmar o estado na API e só então mover dinheiro, sempre de forma idempotente. Esta página traz a ordem exata dos 13 passos, o que cada resposta HTTP provoca no Dracma e o pseudocódigo de referência. ### O que é A ordem dos passos é a do **operador de referência** que a suíte de conformidade usa como oráculo ([Suíte de conformidade](../sandbox/conformidade.md)). Seguir a ordem importa: a verificação barata e a de segurança vêm antes do trabalho caro (consulta à API e efeito no livro). ```mermaid flowchart TD A[Chega o POST] --> B{1-4: corpo lido,
headers, janela, timestamp} B -- falha --> R401[401 ou 400] B -- ok --> C{5: algum v1 confere
com algum segredo ativo?} C -- não --> R401 C -- sim --> D{6-7: JSON válido e
Event-Id igual ao id?} D -- não --> R400[400] D -- sim --> E{8: já processado?} E -- sim --> R200a[200 sem efeito] E -- não --> F[9: fetch-back na API] F --> G{erro de rede ou 5xx?} G -- sim --> R503[503: o Dracma reentrega] G -- não --> H{10: coerente com o que
você registrou?} H -- não --> R200b[Alerta, sem crédito, 200] H -- sim --> I{11: versão maior que
a aplicada?} I -- não --> R200a I -- sim --> J[12: aplica o efeito
idempotente] J --> K[13: marca visto e responde 200] ``` ### Quando usar Use esta página como especificação ao escrever ou revisar o receptor, e como roteiro de teste: cada linha da tabela de passos é um caso a cobrir. Os [vetores de assinatura](requisicao-e-assinatura.md) cobrem os passos 1 a 5. ### Como funciona #### Os 13 passos, nesta ordem | # | Passo | Se falhar | |---|---|---| | 1 | Leia o corpo **cru**, com um limite de tamanho folgado (o operador de referência lê até 64 KiB; sugestão: 256 KiB). | 400 | | 2 | Confira se os headers `Dracma-Timestamp` e `Dracma-Signature` estão presentes. Interprete `Dracma-Signature` como pares `chave=valor` separados por vírgula: exatamente um `t=` e pelo menos um `v1=`. Ignore chaves desconhecidas, que podem ser versões futuras. | **401** | | 3 | **Janela:** `abs(agora − t) ≤ 300 s`. O relógio do seu servidor precisa estar sincronizado por NTP. | **401** | | 4 | O valor de `Dracma-Timestamp` tem de ser **igual** ao `t=`. | **401** | | 5 | **Assinatura:** aceite se **qualquer** `v1` bater com **qualquer** segredo de webhook ativo. Use comparação em tempo constante e percorra todos os pares, sem sair do laço no primeiro acerto. | **401** | | 6 | Interprete o JSON. | 400 | | 7 | `Dracma-Event-Id` tem de ser **igual** ao `id` do corpo. | **400** | | 8 | **Deduplicação:** se o `Dracma-Event-Id` já foi processado com sucesso, responda **200 sem efeito**. Guarde os ids por **no mínimo 7 dias**. | 200 | | 9 | **Fetch-back:** para eventos de depósito ou saque, faça `GET /v1/deposits/{object_id}` ou `GET /v1/withdrawals/{object_id}` com a **sua** credencial. | Erro de rede ou 5xx: responda **503** para o Dracma reentregar. 404: não mova dinheiro, marque o evento como visto e responda 200. | | 10 | **Coerência:** confira que o `external_id` foi criado por você e que `amount`, `currency` e `player_ref` são **idênticos** aos que você registrou. | Em divergência: **não credite**, marque o caso para revisão, dispare um alerta e responda 200. | | 11 | **Versão:** se `version` for menor ou igual à última versão que você aplicou para esse objeto, não faça nada e responda 200. | 200 | | 12 | Aplique o efeito do **estado devolvido pela API** (veja a tabela abaixo). O efeito precisa ser idempotente pelo `external_id`: no máximo um crédito por depósito. | Erro interno: 5xx (reentrega) | | 13 | **Só depois** do sucesso, marque o `Dracma-Event-Id` como visto e responda **200**. | | A tabela de respostas na seção seguinte diz o que o Dracma faz com cada código. #### O que cada resposta provoca no Dracma | Você responde | O Dracma faz | |---|---| | `2xx` (200 a 299), em até 10 s | Dá a entrega como feita (`delivered`). Nada mais é enviado para esse evento. | | `3xx` | **Não segue o redirect.** Conta como falha e repete. | | `400`, `401`, `403`, `404`, `410`, `413`, `422` | Conta como falha e **repete** no cronograma, por até 72 h. Nenhum 4xx para a retentativa. | | `429` | Falha, repete no cronograma normal. O Dracma não lê `Retry-After`. | | `5xx` (por exemplo `503`) | Falha, repete no cronograma. | | Sem resposta em 10 s, conexão recusada, erro de TLS | Falha, repete. | O cronograma, a janela de 72 h e a DLQ estão em [Retentativas, skipped e DLQ](retentativas-e-dlq.md). Pontos de atenção: - **Responda em até 10 s.** Depois disso o Dracma trata a tentativa como falha, mesmo que o seu servidor termine o trabalho. Trabalho pesado vai para uma fila local; só responda 200 depois de gravar o evento de forma durável. - **O corpo da resposta é ignorado.** Só o código de status importa; o Dracma descarta o corpo (lê no máximo 64 KiB). - **Erro "permanente" não existe para o Dracma.** Responder `400` ou `401` não faz o Dracma desistir: o evento volta até 15 ou 16 vezes e vai para a DLQ, com alerta. Para um evento que você não quer tratar, responda **200**. - **Nunca devolva a assinatura esperada nem o segredo** na mensagem de erro. !!! note "Texto antigo da referência" A descrição do webhook na referência OpenAPI dizia que, em caso de `401`, o Dracma "não insiste além da política normal e alerta". Isso induz a erro: o `401` é uma falha como qualquer outra, repetida até a DLQ. O alerta é o e-mail de DLQ ao fim do ciclo. #### Fetch-back: por que é obrigatório Suponha que o seu segredo de webhook vazou. Com ele alguém forja um `deposit.paid` com assinatura válida. Como você **não credita pelo corpo** e sim pelo `GET` feito com a sua credencial, o webhook forjado não passa de uma consulta inútil, e nenhum dinheiro é criado. A suíte de conformidade testa exatamente isto: um webhook forjado com assinatura válida não pode gerar crédito. O fetch-back também resolve a ordem: a consulta devolve sempre o estado mais recente, e o `version` do objeto ordena as aplicações. #### Estado devolvido → ação O que manda é **sempre o estado devolvido pela consulta**, não o `type` do evento. A tabela completa por evento está em [Catálogo de eventos](catalogo-de-eventos.md); o resumo por estado é: | Estado do objeto | Ação | |---|---| | Depósito `pending` | Nenhum dinheiro se move. Mostre o QR Code. | | Depósito `under_review` | Nenhum dinheiro se move. O desfecho vem depois. | | Depósito `paid` | Credite o jogador pelo valor que **você registrou**, uma única vez. | | Depósito `expired`, `cancelled`, `failed`, `returned` | Terminal, sem crédito. | | Depósito `refunded`, `chargeback` | Débito de depósito creditado: política sua; recomendação de decisão manual com alerta. | | Saque `under_review`, `approved`, `processing` | Nenhum dinheiro se move; a reserva continua. | | Saque `paid` | A reserva vira saída definitiva; guarde `psp_reference`. | | Saque `rejected`, `failed`, `cancelled` | Devolva a reserva ao jogador, uma única vez. | | Saque `returned` | O banco devolveu depois do `paid`: política sua; recomendação de decisão manual com alerta. | | Qualquer estado ou tipo **desconhecido** | Responda 200, grave o evento, não mova dinheiro e confirme depois. | #### Idempotência entre "visto" e efeito A gravação do "evento visto" e o efeito no seu livro precisam ser **idempotentes entre si**. Há duas formas de garanti-lo: gravar os dois na mesma transação, ou tornar o efeito idempotente pelo `external_id` (uma restrição de unicidade no lançamento). Assim, um crash entre um passo e outro não credita duas vezes. A segunda forma é a que protege também contra o mesmo evento chegando por webhook e por [`GET /events`](recuperacao-get-events.md). #### Tamanho do corpo O Dracma não impõe um tamanho máximo ao corpo que envia. Na prática, o corpo é o envelope com um depósito ou um saque, alguns KiB, e o operador de referência da conformidade lê até 64 KiB. Configure o limite de leitura do receptor com folga (a sugestão do passo 1 é 256 KiB) e **não** responda com um erro de tamanho a um evento legítimo: como qualquer não-2xx, ele seria repetido até a DLQ. ### Exemplos #### Pseudocódigo ```text handler(req): corpo = ler_cru(req, max=256KiB) ou 400 sig = parse(req.header["Dracma-Signature"]) ou 401 # t= único, >=1 v1 se |agora - sig.t| > 300: 401 se req.header["Dracma-Timestamp"] != sig.t: 401 ok = falso para cada segredo em segredos_webhook_ativos: # rotação: 1 ou 2 esperado = HMAC_SHA256(segredo, sig.t + "." + corpo) para cada v em sig.v1: se tempo_constante_igual(hex_decode(v), esperado): ok = verdadeiro # sem break se não ok: 401 ev = json(corpo) ou 400 se ev.id != req.header["Dracma-Event-Id"]: 400 se ja_processado(ev.id): 200 # dedup >= 7 dias se ev.type começa com "deposit." ou "withdrawal.": obj, status = GET /v1/{deposits|withdrawals}/{ev.object_id} # SUA credencial se erro ou status >= 500: 503 # Dracma reentrega se status == 404: marcar_visto(ev.id); 200 se não coerente(obj): revisar+alertar; marcar_visto(ev.id); 200 se obj.version <= versao_aplicada(obj.id): marcar_visto(ev.id); 200 aplicar_estado(obj) # idempotente por external_id, na mesma transação de: marcar_visto(ev.id) 200 ``` Há clientes executáveis com `verificarWebhook` em Go, Node, PHP e shell em [Exemplos executáveis](../exemplos/README.md). #### Tabelas mínimas ```sql -- eventos já aplicados (retenção de pelo menos 7 dias) CREATE TABLE dracma_eventos_vistos ( event_id text PRIMARY KEY, visto_em timestamptz NOT NULL DEFAULT now() ); -- o efeito financeiro nunca se repete para o mesmo depósito ou saque CREATE UNIQUE INDEX lancamento_dracma_unico ON lancamentos (external_id, tipo); ``` ### Erros e casos de borda - **O receptor responde 401 a todos os eventos de repente.** Quase sempre é rotação de segredo (o receptor só conhece um dos dois) ou relógio fora de sincronia. Veja [Problemas comuns](troubleshooting.md). - **Chegou um evento que você nunca criou.** A coerência (passo 10) pega: `external_id` desconhecido. Não credite, alerte e responda 200. - **O fetch-back devolve 404.** O objeto não existe para a sua chave (pode ser de outra operação, ou um evento forjado). Não mova dinheiro; marque como visto e responda 200. - **O mesmo evento chega duas vezes ao mesmo tempo.** Dois processos podem passar pelo passo 8 juntos. A restrição de unicidade no lançamento (passo 12) é a barreira final. - **A fila local enche.** Responda 2xx só depois de gravar; se a fila não aceita mais, responda 503 e deixe o Dracma repetir. - **Fuso e relógio.** `created_at` e `Dracma-Timestamp` estão em UTC. ### Veja também - [Requisição e assinatura](requisicao-e-assinatura.md): o cálculo da assinatura e os vetores. - [Envelope](envelope.md) e [Catálogo de eventos](catalogo-de-eventos.md): o que cada campo e cada evento significa. - [Retentativas, skipped e DLQ](retentativas-e-dlq.md): cronograma de reentrega. - [Recuperação com `GET /events`](recuperacao-get-events.md): job que fecha lacunas. - [Estados no seu produto](../conector/estados-no-produto.md): o que mostrar ao jogador em cada estado. --- # Arquivo: docs/webhooks/retentativas-e-dlq.md ## Retentativas, skipped e DLQ > Para: Dev · Adm — Conferido em 2026-10-18. Toda resposta que não seja 2xx faz o Dracma tentar de novo, com intervalos crescentes e um pequeno acréscimo aleatório, por até 72 horas. Depois disso o evento vai para a DLQ e continua recuperável. Esta página traz o cronograma, os estados de uma entrega, o que é `skipped` e como reabrir uma entrega. ### O que é Uma **entrega** é o registro de um evento a enviar para a URL da operação; uma **tentativa** é um `POST` dessa entrega. Cada tentativa tem **10 segundos** no total (conexão, TLS e resposta). O resultado é: - **Sucesso:** qualquer resposta `2xx` (200 a 299). - **Falha:** qualquer outra coisa: `3xx` (o redirect não é seguido), `4xx`, `5xx`, estouro dos 10 s, erro de rede ou de TLS, nome que não resolve ou resolve para endereço bloqueado. Nenhum código de status é "definitivo". Um `400`, `401` ou `410` do seu receptor **não** faz o Dracma desistir: ele repete no cronograma até a DLQ. O corpo da resposta é lido até 64 KiB e descartado; só o status importa. #### Estados de uma entrega | Estado | Significado | |---|---| | `pending` | Aguardando a primeira tentativa ou a próxima. | | `delivered` | O seu receptor respondeu 2xx. | | `dlq` | As tentativas se esgotaram sem 2xx. | | `skipped` | O evento foi registrado, mas nada foi enviado (veja abaixo). | ```mermaid stateDiagram-v2 [*] --> pending: URL cadastrada e tipo assinado [*] --> skipped: sem URL, outro ambiente ou tipo não assinado pending --> delivered: 2xx pending --> pending: falha, próxima tentativa agendada pending --> dlq: a próxima tentativa passaria de 72 h pending --> skipped: ambiente da URL difere do do evento dlq --> pending: reentrega (novo ciclo de 72 h) skipped --> pending: reentrega no sandbox delivered --> [*] ``` ### Quando usar Leia esta página para dimensionar o receptor (quanto tempo ele pode ficar fora do ar sem perder entregas), para entender por que um evento "não chegou" e para recuperar uma DLQ. O que fazer quando um evento esgota o ciclo está na seção [DLQ](#dlq-evento-que-esgotou-as-tentativas). ### Como funciona #### Cronograma e jitter Depois de cada tentativa que falha, a próxima é agendada para **espera-base + jitter** a partir do horário **agendado** da tentativa anterior. A espera-base é: **30 s, 2 min, 10 min, 30 min, 1 h, 2 h, 4 h, 8 h** e, depois disso, **8 h** a cada tentativa. O **jitter** é um acréscimo aleatório, **só positivo**, de 0 a 10 % da espera-base, em segundos inteiros. Ele espalha a carga no seu receptor; a espera-base é um piso (nenhuma tentativa sai antes dela). A tabela mostra o instante de cada tentativa, contado desde a primeira, sem jitter e com o jitter máximo: | Tentativa | Espera-base | Jitter | Instante (sem jitter) | Instante (jitter máximo) | |---|---|---|---|---| | 1 | imediata | n/a | 0:00:00 | 0:00:00 | | 2 | 30 s | até +3 s | 0:00:30 | 0:00:33 | | 3 | 2 min | até +12 s | 0:02:30 | 0:02:45 | | 4 | 10 min | até +1 min | 0:12:30 | 0:13:45 | | 5 | 30 min | até +3 min | 0:42:30 | 0:46:45 | | 6 | 1 h | até +6 min | 1:42:30 | 1:52:45 | | 7 | 2 h | até +12 min | 3:42:30 | 4:04:45 | | 8 | 4 h | até +24 min | 7:42:30 | 8:28:45 | | 9 | 8 h | até +48 min | 15:42:30 | 17:16:45 | | 10 | 8 h | até +48 min | 23:42:30 | 26:04:45 | | 11 | 8 h | até +48 min | 31:42:30 | 34:52:45 | | 12 | 8 h | até +48 min | 39:42:30 | 43:40:45 | | 13 | 8 h | até +48 min | 47:42:30 | 52:28:45 | | 14 | 8 h | até +48 min | 55:42:30 | 61:16:45 | | 15 | 8 h | até +48 min | 63:42:30 | 70:04:45 | | 16 | 8 h | até +48 min | 71:42:30 | não ocorre | Os instantes estão em horas:minutos:segundos desde a primeira tentativa. O Dracma verifica as entregas vencidas em ciclos curtos, então o horário real de envio pode se atrasar um pouco além do agendado; esse atraso não é cumulativo, porque a contagem parte do horário agendado. #### A janela de 72 horas O ciclo tem **72 horas** a partir da primeira tentativa. O Dracma **nunca** agenda uma tentativa depois dessas 72 h: se a próxima passaria do limite, a entrega vai para a DLQ. Na prática: - um ciclo tem **15 ou 16 tentativas** no total (a 16ª só existe se o jitter acumulado ficar abaixo de cerca de 17 minutos, o que é raro); - a última tentativa sai entre cerca de 63 h 42 min e 71 h 42 min depois da primeira; - o evento entra na DLQ logo depois de a última tentativa falhar, portanto **antes** das 72 h. Não dependa do horário exato de nenhuma tentativa. Se o seu receptor ficar indisponível por menos de cerca de 63 horas, a entrega ainda acerta quando ele voltar. #### Concorrência e rajadas Entregas de eventos diferentes são independentes e concorrem entre si. O Dracma não oferece limite de vazão por endpoint nem fila ordenada por objeto. Uma rajada (por exemplo, muitos depósitos pagos ao mesmo tempo) chega como muitas requisições em sequência rápida, e as tentativas pendentes de eventos antigos misturam-se com as de eventos novos. Dimensione o receptor para absorver o pico: grave o evento em uma fila local e responda 2xx rápido. !!! warning "Não disponível" O Dracma não permite configurar limite de taxa, concorrência máxima ou janela de entrega por endpoint. #### `skipped`: o evento foi registrado e nada foi enviado Uma entrega fica `skipped` (com um `skip_reason`) quando o Dracma decide, por regra, **não enviar**: | `skip_reason` | Quando | |---|---| | `no_endpoint` | A operação não tinha URL cadastrada quando o evento nasceu. | | `environment_mismatch` | O ambiente da URL (teste ou real) difere do ambiente do evento. | | `event_not_subscribed` | A URL tem uma lista de tipos assinados e o tipo do evento não está nela. | Três pontos que costumam surpreender: - **Não é retroativo.** Eventos que nasceram antes de você cadastrar a URL ficam `skipped` para sempre; cadastrar a URL depois **não** os envia. Recupere-os por [`GET /events`](recuperacao-get-events.md). - **Filtro de tipos.** A assinatura de eventos é uma lista de tipos do [Catálogo de eventos](catalogo-de-eventos.md). **Lista vazia significa todos os tipos.** Um tipo fora da lista nunca é enviado por webhook, mas aparece em `GET /events`. Quem assina só `deposit.paid` não recebe `deposit.under_review` nem `deposit.expired`; o job de recuperação precisa tratar todos os tipos. - **Nada é retentado.** `skipped` não entra no cronograma e não vai para a DLQ. #### DLQ: evento que esgotou as tentativas Quando o ciclo termina sem 2xx, a entrega passa a `dlq`. O evento **não se perde**: o objeto continua consultável na API, o evento continua em `GET /events` e a entrega pode ser reaberta. O ciclo de vida: 1. **Alerta.** Se a operação tem endereços de alerta cadastrados (até 5), o Dracma envia um e-mail "Webhook em DLQ" com o tipo e o id do evento. Sem endereços de alerta, nenhum e-mail sai. 2. **Visibilidade.** A entrega aparece na aba de DLQ do painel, com o histórico de tentativas (número, horário, status HTTP, duração e erro resumido). Veja [Webhooks no painel](../painel/webhooks.md#aba-dlq). 3. **Reentrega.** No painel, quem tem permissão de gerenciar webhooks pode pré-visualizar e reentregar eventos da DLQ, até 50 por vez. A reentrega reabre um **ciclo novo de 72 h**, com o **mesmo** `Dracma-Event-Id` e os **mesmos bytes** de corpo, para a URL que estiver cadastrada **naquele momento** (nunca para outra URL). 4. **Resolução.** Um evento da DLQ que você tratou por outro caminho (por exemplo, pelo job de `GET /events`) pode ser marcado como **resolvido**, com um motivo de 10 a 1000 caracteres. Isso não reenvia nada; só tira o item da lista de pendentes e guarda a justificativa. !!! note "Reentrega de evento já entregue" No painel, só eventos que estão na DLQ podem ser reentregues. Um evento já `delivered` não é reaberto em produção. Na operação de teste, a ação de reentrega do sandbox reabre eventos em qualquer estado (`pending`, `skipped`, `dlq` ou `delivered`); veja [Sandbox](../sandbox/README.md). Como a reentrega manual pode acontecer muito depois do evento original, a sua deduplicação não deve depender só de uma memória curta. Guarde os `Dracma-Event-Id` por **no mínimo 7 dias** (o ciclo de 72 h mais folga para uma reentrega) e mantenha o efeito idempotente também pelo `external_id`, que é uma segunda barreira. ### Exemplos **Receptor fora do ar por 3 horas.** A primeira tentativa falha às 14:00:00. As seguintes saem por volta de 14:00:30, 14:02:30, 14:12:30, 14:42:30, 15:42:30 e 17:42:30 (somando o jitter, cada uma pode atrasar até 10 % da espera). Se o receptor voltar às 17:00, a tentativa de 17:42 já recebe o 2xx e a entrega fica `delivered`. **Receptor com bug permanente.** Se o seu receptor responde `400` para todo evento, o Dracma repete 15 ou 16 vezes em cerca de três dias e então coloca o evento na DLQ, com e-mail de alerta. Corrija o bug, reentregue a DLQ pelo painel e confirme que o job de `GET /events` não deixou lacuna. **URL cadastrada depois do primeiro depósito.** Os eventos anteriores ao cadastro ficam `skipped` com `no_endpoint`. Eles não chegam por webhook; o job de `GET /events` os entrega ao seu receptor. ### Erros e casos de borda - **Falha sem código HTTP.** A tentativa pode falhar antes de o `POST` sair: a URL deixou de passar na política, o nome resolve para um endereço bloqueado ou a operação **não tem segredo de webhook ativo** (nada é enviado sem assinatura). O erro aparece no log de entregas e a tentativa conta no cronograma. Gere o primeiro segredo antes de esperar eventos (veja [Segredo e rotação](segredos-e-rotacao.md)). - **2xx com leitura da resposta interrompida.** Se a conexão cair no meio da resposta, mesmo com status 2xx, a tentativa pode ser contada como falha. Responda curto e feche a conexão normalmente. - **Erros "permanentes" geram tempestade.** Um receptor que responde 4xx para um evento que nunca vai aceitar recebe 15 ou 16 tentativas e um alerta de DLQ. Para um evento que você não quer tratar, responda 200. - **Corpo da resposta.** Não devolva informação útil esperando que o Dracma a use; o corpo é descartado. - **Os dados mudaram entre tentativas.** O corpo é o mesmo em todas; para saber o estado atual, consulte a API. ### Veja também - [Recuperação com `GET /events`](recuperacao-get-events.md): fechar lacunas de `skipped` e de DLQ. - [Webhooks no painel](../painel/webhooks.md): log de entregas, saúde e DLQ. - [Alertas e e-mails](../painel/alertas.md): endereços de alerta. - [Receptor passo a passo](receptor.md): o que responder em cada caso. - [Problemas comuns](troubleshooting.md): sintoma, causa e ação. --- # Arquivo: docs/webhooks/recuperacao-get-events.md ## `GET /events` > Para: Dev — Conferido em 2026-10-18. `GET /v1/events` devolve os eventos da sua operação, em ordem de criação, com o mesmo envelope do webhook. É a rede de segurança dos webhooks: com um job a cada 5 minutos você recupera qualquer evento perdido, esgotado na DLQ, não assinado ou anterior ao cadastro da URL. ### O que é Uma listagem paginada por cursor, só de leitura, com a mesma autenticação das demais rotas da API ([Autenticação HMAC](../api/autenticacao.md)). Ela exige uma chave com escopo de leitura e devolve **apenas eventos da operação da chave**: um evento de outra operação nunca aparece. ```http GET /v1/events?after=evt_5a9c1f0e7b3d4c28a6e1f4b07d92c385&limit=200 GET /v1/events?object_id=dep_0198b7c24d5e7a1b8c2d3e4f5a6b7c8d ``` | Parâmetro | Regra | |---|---| | `after` | Id de evento (`evt_…`) **da sua operação**. Devolve os eventos criados depois dele. Sem `after`, a listagem começa no primeiro evento da operação. | | `object_id` | Filtra os eventos de um objeto (`dep_…` ou `wdr_…`). Aceita letras, dígitos, `_` e `-`, de 1 a 64 caracteres. | | `limit` | De 1 a 200; padrão 50. | A resposta é `{ "data": [Evento…], "next_cursor": "evt_…" | null }`. `next_cursor` traz o id do último evento da página **quando há mais eventos depois dele**; use-o como `after` da próxima chamada. `null` significa que acabou. Veja o formato do evento em [Envelope](envelope.md). ### Quando usar Rode um **job a cada 5 minutos**, a partir do último evento que você **processou**, e guarde esse cursor no seu banco. Esse job fecha as lacunas de: - webhook perdido por falha de rede ou queda do seu receptor; - evento na DLQ depois de 72 h sem 2xx; - evento que não chegou por webhook porque o tipo não está na sua assinatura (`event_not_subscribed`); - evento anterior ao cadastro da URL (`no_endpoint`); - evento de ambiente diferente do da URL cadastrada. Use também `object_id` para investigar um objeto específico: "o que o Dracma registrou para este depósito?". !!! note "Eventos filtrados continuam aqui" A assinatura por tipo de evento só controla o que sai por **webhook**. `GET /events` devolve todos os eventos do catálogo, tenham ou não sido entregues. Se você assina só alguns tipos, o job precisa tratar todos. ### Como funciona #### Ordem, cursor e página Os eventos vêm em **ordem de criação** (desempate estável por id). O cursor é o id de um evento: o Dracma busca a posição desse evento e devolve os posteriores, de modo que novos eventos criados enquanto você pagina entram no fim e não deslocam as páginas. Cada evento é gravado na mesma transação que muda o estado do objeto, então a lista não perde uma transição. ```mermaid sequenceDiagram participant J as Seu job (a cada 5 min) participant A as API do Dracma J->>A: GET /v1/events com after = último processado, limit 200 A-->>J: data[] + next_cursor loop para cada evento J->>J: deduplica, consulta o objeto, aplica o efeito J->>J: grava o cursor (id do evento) end alt next_cursor presente J->>A: GET /v1/events com after = next_cursor else next_cursor nulo J->>J: fim da rodada end ``` #### Sem assinatura individual Os eventos de `GET /events` **não vêm assinados um a um**: a autenticidade vem da **sua** chamada autenticada à API. Mesmo assim, passe cada evento pelo mesmo caminho do webhook, a partir da deduplicação: dedup por `id`, consulta do objeto (fetch-back), coerência, versão e efeito idempotente. O `Dracma-Event-Id` de um webhook é o `id` do mesmo evento em `GET /events`, então uma entrega por webhook e uma por listagem do mesmo evento são deduplicadas juntas. #### Limite de taxa `GET /events` está no balde de **listagem**: 5 requisições por segundo por chave. Pagine com `limit=200` e uma cadência calma; não varra tudo a cada execução do job. Ao exceder o limite você recebe `429` com `Retry-After`; veja [Limites e quotas](../api/limites.md#baldes-de-taxa). #### Retenção !!! warning "Não disponível" O Dracma não publica um prazo de retenção para os eventos de `GET /events`, e portanto não o garante. Mantenha o job de 5 minutos funcionando para nunca depender de reler eventos antigos, e guarde no seu banco o que precisar auditar. ### Exemplos Resposta com dois eventos do mesmo depósito (dados fictícios; a resposta de produção traz até `limit` eventos): ```json { "data": [ { "id": "evt_686599cbb91280cbb34334f16f643047", "type": "deposit.pending", "created_at": "2026-10-18T14:00:02Z", "operation_id": "0198b7c2-3d4e-7f50-8a61-92b3c4d5e6f7", "object_id": "dep_0198b7c24d5e7a1b8c2d3e4f5a6b7c8d", "object_version": 2, "data": { "object": { "id": "dep_0198b7c24d5e7a1b8c2d3e4f5a6b7c8d", "external_id": "pedido-10482", "status": "pending", "version": 2, "amount": { "amount": "10000", "currency": "BRL" }, "paid_amount": null, "method": "pix", "player_ref": "jogador-7731", "payer": null, "instructions": { "pix": { "copy_paste": "00020126580014br.gov.bcb.pix0136exemplo-chave-ficticia5204000053039865406100.005802BR5909EXEMPLO6009SAO PAULO62070503***6304ABCD", "expires_at": "2026-10-18T14:30:00Z" } }, "failure_code": null, "review_reasons": [], "created_at": "2026-10-18T14:00:00Z", "updated_at": "2026-10-18T14:00:02Z", "paid_at": null, "expires_at": "2026-10-18T14:30:00Z" } } }, { "id": "evt_e12d177892007213962d29c4b3b60580", "type": "deposit.paid", "created_at": "2026-10-18T14:05:13Z", "operation_id": "0198b7c2-3d4e-7f50-8a61-92b3c4d5e6f7", "object_id": "dep_0198b7c24d5e7a1b8c2d3e4f5a6b7c8d", "object_version": 3, "data": { "object": { "id": "dep_0198b7c24d5e7a1b8c2d3e4f5a6b7c8d", "external_id": "pedido-10482", "status": "paid", "version": 3, "amount": { "amount": "10000", "currency": "BRL" }, "paid_amount": { "amount": "10000", "currency": "BRL" }, "method": "pix", "player_ref": "jogador-7731", "payer": { "document": { "type": "CPF", "number": "12345678909", "country": "BR" }, "name": "Maria Exemplo", "matches_player": true }, "instructions": { "pix": { "copy_paste": "00020126580014br.gov.bcb.pix0136exemplo-chave-ficticia5204000053039865406100.005802BR5909EXEMPLO6009SAO PAULO62070503***6304ABCD", "expires_at": "2026-10-18T14:30:00Z" } }, "failure_code": null, "review_reasons": [], "created_at": "2026-10-18T14:00:00Z", "updated_at": "2026-10-18T14:05:13Z", "paid_at": "2026-10-18T14:05:12Z", "expires_at": "2026-10-18T14:30:00Z" } } } ], "next_cursor": "evt_e12d177892007213962d29c4b3b60580" } ``` Resposta de `400` para um `after` desconhecido ou de outra operação (o Dracma não confirma se o id existe em outra operação): ```json { "type": "https://docs.dracma.test/errors/validation_failed", "title": "validation_failed", "status": 400, "code": "validation_failed", "trace_id": "4bf92f3577b34da6a3ce929d0e0e4736", "errors": [ { "field": "after", "message": "parâmetro inválido", "reason": "unknown" } ] } ``` Esqueleto do job (a função `aplicar_evento` é a mesma do receptor, a partir da deduplicação): ```python def rodada(banco, api): cursor = banco.ler_cursor() # último evt processado, ou None while True: params = {"limit": 200} if cursor: params["after"] = cursor pagina = api.get("/v1/events", params) # com a sua chave for ev in pagina["data"]: aplicar_evento(ev) # dedup, fetch-back, coerência, versão, efeito banco.gravar_cursor(ev["id"]) # só depois de aplicar cursor = ev["id"] if pagina["next_cursor"] is None: break ``` ### Erros e casos de borda | Resposta | Quando | |---|---| | `400 validation_failed`, `errors[0].reason = "format"` | `after` fora do formato `evt_` mais letras e dígitos, ou `object_id` com caracteres inválidos. | | `400 validation_failed`, `errors[0].reason = "unknown"` | `after` de um evento que não existe ou que pertence a outra operação. | | `400 validation_failed`, `errors[0].reason = "range"` | `limit` fora de 1 a 200 ou não numérico. | | `401` | Chave ou assinatura da chamada inválida. | | `429` | Mais de 5 requisições por segundo na listagem. Respeite `Retry-After`. | | `503` | Dependência indisponível. Tente de novo; a resposta traz `Retry-After`. | - **Cursor perdido:** se você perdeu o cursor, recomece de um evento conhecido ou do início e deixe a deduplicação descartar o que já foi aplicado. É seguro, só mais lento. - **Lista vazia com `next_cursor` nulo** significa que não há eventos novos. - **Eventos de objetos terminais** continuam na lista; o job não deve tratá-los como novos efeitos se a versão já foi aplicada. - **Timestamps:** o `created_at` de eventos de saque pode trazer frações de segundo. Veja [Envelope](envelope.md). ### Veja também - [Receptor passo a passo](receptor.md): o caminho que cada evento percorre. - [Retentativas, skipped e DLQ](retentativas-e-dlq.md): por que um evento pode não chegar por webhook. - [Paginação e filtros](../api/paginacao.md): convenções de cursor da API. - [Limites e quotas](../api/limites.md): limites de taxa. --- # Arquivo: docs/webhooks/segredos-e-rotacao.md ## Segredo e rotação > Para: Dev · Adm — Conferido em 2026-10-18. O segredo de webhook (`whsec_…`) é a chave com que o Dracma assina cada entrega e com que o seu receptor a verifica. O primeiro segredo só existe depois que você clica em "Rotacionar segredo" no painel. A rotação mantém o segredo anterior por 7 dias, e "revogar" um segredo vazado é, na prática, rotacionar duas vezes. ### O que é | Propriedade | Valor | |---|---| | Formato | `whsec_` seguido de 64 caracteres hexadecimais (32 bytes aleatórios de um gerador criptográfico). | | Quem gera | O Dracma, ao rotacionar. Você não escolhe nem importa o valor. | | Uso | Chave do HMAC-SHA256 que assina cada entrega. Veja [Requisição e assinatura](requisicao-e-assinatura.md). | | Quantos | No máximo **2 ativos** por operação ao mesmo tempo. | | Exibição | **Uma única vez**, na resposta da rotação. O painel nunca mostra o valor de novo; só o id, a data de criação, a expiração e o estado de cada segredo. | | Em repouso | Guardado cifrado no Dracma. | O segredo de webhook é **outro** segredo, diferente do segredo de chamada da API (`sk_…`). Guarde os dois no cofre, cada um separado, e nunca no navegador nem em código de frontend. ### Quando usar #### O primeiro segredo !!! warning "O segredo não chega pronto" Ao cadastrar a URL, o Dracma **não** cria um segredo. O primeiro `whsec_` só existe depois de "Rotacionar segredo" na tela de webhooks do painel, feito por quem tem permissão de gerenciar webhooks (no onboarding, o administrador do seu tenant). Não há envio do segredo por e-mail nem por canal fora do painel. Sem segredo ativo, o Dracma **não envia** a entrega: a tentativa falha antes de sair, e o evento percorre o cronograma até a DLQ. Por isso: 1. Cadastre a URL ([Seu endpoint](endpoint-e-politica-de-url.md)). 2. Rotacione o segredo, copie o valor **na hora** e guarde-o no cofre do receptor. 3. Só então gere eventos. #### Rotação A rotação exige: - permissão de gerenciar webhooks na operação; - confirmação de identidade adicional no painel (*step-up*); - **uma pessoa**: diferente da mudança de URL, a rotação **não** precisa de aprovação de uma segunda pessoa nem de espera. A rotação vale imediatamente. Todas as ações ficam na trilha de auditoria do painel, sem o valor do segredo. ### Como funciona #### Sobreposição de 7 dias Quando você rotaciona: - o segredo **novo** passa a assinar imediatamente; - o segredo que era o mais novo até agora vira o **anterior** e continua assinando por **7 dias**; depois ele expira; - qualquer segredo mais antigo que o anterior expira **na hora**. Enquanto os dois valem, cada entrega leva **dois** `v1`, o do segredo novo primeiro. O seu receptor mantém uma **lista de segredos ativos** (1 ou 2 itens) e aceita se qualquer `v1` bater com qualquer segredo da lista, em tempo constante e sem sair do laço no primeiro acerto. ```mermaid sequenceDiagram participant P as Painel (você) participant D as Dracma participant R as Seu receptor P->>D: Rotacionar segredo D-->>P: whsec_NOVO (exibido uma vez) P->>R: Adiciona whsec_NOVO à lista de segredos (cofre ou deploy) Note over D,R: Por 7 dias, cada entrega traz v1 do NOVO e v1 do ANTERIOR D->>R: POST com dois v1 R-->>D: 200 (qualquer v1 bate) Note over R: Depois de confirmar, remove o ANTERIOR da lista Note over D: Ao fim de 7 dias o ANTERIOR expira e só o NOVO assina ``` #### Procedimento sem perder entrega 1. **Rotacione** no painel e copie o `whsec_` novo. 2. **Adicione** o segredo novo à lista do receptor pelo cofre ou pelo pipeline de deploy, nunca à mão no servidor. O receptor passa a aceitar os dois. 3. **Confirme** que os webhooks continuam sendo aceitos (o log de entregas do painel mostra `2xx`). 4. **Remova** o segredo anterior da lista do receptor, depois de confirmar. O Dracma continua enviando dois `v1` até o anterior expirar; o receptor com um segredo só ignora o `v1` que não conhece. Você pode inverter os passos 2 e 3 sem risco: o receptor que só conhece o segredo novo aceita o 1º `v1`, e o que ainda só conhece o anterior aceita o 2º. Foi pensado para que a rotação nunca derrube a integração. #### Quando rotacionar !!! note "Recomendação, não regra" O Dracma recomenda rotacionar o segredo de webhook periodicamente, por exemplo a cada 90 dias. **Nenhuma regra do Dracma impõe isso**: o segredo não expira sozinho e o painel não envia lembrete. A rotina de rotação é responsabilidade da sua equipe. Rotacione também quando alguém que conhecia o segredo sair do projeto e sempre que houver suspeita de vazamento. ### Exemplos Linha do tempo de um segredo, com duas rotações seguidas (`S1` é o segredo original): | Momento | `S1` | `S2` | `S3` | Segredos que assinam | |---|---|---|---|---| | Antes da 1ª rotação | ativo | n/a | n/a | `S1` | | 1ª rotação (dia 0) | expira no dia 7 | ativo | n/a | `S2`, `S1` (dois `v1`) | | Dia 7 | expirado | ativo | n/a | `S2` | | 2ª rotação (no dia 0, logo depois da 1ª) | **expira agora** | expira no dia 7 | ativo | `S3`, `S2` | Estados que o painel mostra para cada segredo: o mais novo está `active`; um segredo anterior com data de expiração futura está `expiring` (a operação fica em "rotação" enquanto houver um). Segredos vencidos deixam de aparecer. ### Erros e casos de borda #### Segredo vazado: "revogar" é rotacionar duas vezes !!! warning "Não disponível" O painel **não** tem uma ação "revogar segredo de webhook". Textos antigos falavam em "revogação de emergência no painel, com efeito em até cerca de 5 minutos"; isso não existe para webhooks. O que protege o seu dinheiro quando o segredo vaza: 1. **O fetch-back.** Quem tem o segredo consegue forjar um webhook com assinatura válida, mas o seu receptor só credita pelo estado que a API devolve para a **sua** credencial. Um webhook forjado não cria dinheiro. Veja [Receptor passo a passo](receptor.md#fetch-back-por-que-e-obrigatorio). 2. **A sua lista de segredos.** O que decide se um webhook é aceito é a lista do **seu** receptor, não o relógio do Dracma. Procedimento de emergência, nesta ordem: 1. **Rotacione** no painel e obtenha o segredo novo. Como explicado acima, o segredo vazado continua assinando no Dracma por 7 dias; isso é irrelevante para a segurança, porque o que importa é o que o seu receptor aceita. 2. **Troque a lista do receptor:** coloque o segredo novo e **remova imediatamente o vazado**. A partir daqui, nenhum webhook assinado só com o segredo vazado é aceito. 3. **Rotacione uma segunda vez** se quiser que o Dracma **pare de assinar** com o segredo vazado agora: a 2ª rotação expira na hora o segredo mais antigo e deixa o anterior por mais 7 dias. Adicione o segredo da 2ª rotação ao receptor. 4. **Procure forjados:** compare os eventos que o seu receptor processou com `GET /events` e com a reconciliação ([Recuperação com `GET /events`](recuperacao-get-events.md)). 5. **Descubra a origem do vazamento** (log, repositório, ferramenta de suporte) e corrija. O passo 2 é o que fecha a janela de aceitação; o passo 1 sozinho não basta. #### Outros casos | Situação | O que acontece | O que fazer | |---|---|---| | Perdeu o valor do segredo | O painel não mostra o valor de novo. | Rotacione e atualize o receptor. | | Receptor responde 401 logo depois da rotação | O receptor só conhece o segredo antigo ou só o novo e testa só o primeiro `v1`. | Mantenha a lista de segredos e teste todos os pares. | | Rotacionou duas vezes por engano | O segredo mais antigo expirou na hora. | Se o receptor só tinha o antigo, adicione os dois mais novos à lista. | | Segredo usado em frontend ou em log | Exposição. | Trate como vazado (procedimento acima). | | Rotação feita por quem não tem permissão | O painel recusa. | Peça ao administrador do tenant. | ### Veja também - [Requisição e assinatura](requisicao-e-assinatura.md): como o segredo entra no HMAC. - [Receptor passo a passo](receptor.md): lista de segredos e fetch-back. - [Webhooks no painel](../painel/webhooks.md): a tela de rotação. - [Segredos e vazamento](../seguranca/segredos-e-credenciais.md): política geral de segredos. --- # Arquivo: docs/webhooks/troubleshooting.md ## Problemas comuns > Para: Dev — Conferido em 2026-10-18. Esta página lista os sintomas mais comuns de uma integração de webhooks, a causa provável e o que fazer. No fim há o que reunir antes de chamar o suporte: `Dracma-Event-Id` e `operation_id`. ### O que é O Dracma não envia nenhum header de rastreamento (`traceparent`) ao seu receptor, e o id interno de trace não sai da casa. A correlação entre o que você viu e o que o Dracma registrou é feita pelo **`Dracma-Event-Id`** (igual ao `id` do corpo) e pelo **`operation_id`**. Registre os dois em todo log do receptor. Quando a assinatura falhar, registre o `Dracma-Event-Id` do header, sem interpretar o corpo. ### Quando usar Use esta página quando um evento não chegar, chegar duplicado, for recusado pelo seu receptor ou aparecer na DLQ. Para cada caso, o log de entregas e a aba de DLQ do painel ([Webhooks no painel](../painel/webhooks.md)) mostram as tentativas, o status HTTP, a duração e o erro resumido (até 200 caracteres) de cada uma. ### Como funciona Siga a ordem: (1) o evento existe? (`GET /events`), (2) o Dracma tentou enviar? (log de entregas), (3) o que o seu receptor respondeu? (status e erro da tentativa), (4) o que o seu receptor fez com a resposta? (seu log). #### O evento não chegou | Sintoma | Causa provável | O que fazer | |---|---|---| | Nenhum webhook, nem de teste | A URL ainda não está efetiva: o pedido de cadastro espera a aprovação de outra pessoa e as 24 h seguintes. | Confira o pedido em [Webhooks no painel](../painel/webhooks.md). | | Nenhum webhook; entregas `skipped` com `no_endpoint` | Os eventos nasceram antes de a URL estar cadastrada. | Eles não são enviados depois. Recupere por [`GET /events`](recuperacao-get-events.md). | | Entregas `skipped` com `event_not_subscribed` | O tipo do evento não está na sua assinatura. | Inclua o tipo na assinatura (lista vazia = todos) e use o job de `GET /events` para o passado. | | Entregas `skipped` com `environment_mismatch` | O evento é de teste e a URL cadastrada é a da operação real, ou o inverso. | Cadastre a URL certa em cada operação. | | Fora de produção, o painel recusa a URL | O domínio não é de teste e não foi liberado. | Peça a liberação do domínio no onboarding. Túneis temporários não funcionam. Veja [Seu endpoint](endpoint-e-politica-de-url.md). | | Tentativas falham sem código HTTP e sem erro do seu servidor | Não há segredo de webhook ativo, o nome resolve para IP bloqueado, o certificado TLS é inválido ou a porta 443 não responde. | Rotacione o primeiro segredo; confira DNS, certificado e firewall. | | Evento do tipo `rules.updated` esperado | Esse evento não existe. | Veja [Catálogo de eventos](catalogo-de-eventos.md). | #### O seu receptor recusa o evento | Sintoma | Causa provável | O que fazer | |---|---|---| | `401` em todos os eventos | Segredo errado (por exemplo, o da operação de teste usado na real: cada operação tem o seu), segredo trocado numa rotação ou relógio do servidor fora de sincronia. | Confira o segredo da operação certa; mantenha a lista de segredos; sincronize o NTP. Veja [Segredo e rotação](segredos-e-rotacao.md). | | `401` só em alguns eventos | Receptor testa só o primeiro `v1` durante uma rotação. | Teste todos os pares `v1` × segredo. | | `401` mesmo com o segredo certo | O corpo foi reserializado ou alterado por proxy, framework ou compressão antes da verificação. | Verifique sobre os bytes crus. Teste com os vetores de [Requisição e assinatura](requisicao-e-assinatura.md). | | `401` só nas reentregas | Relógio fora de sincronia (janela de 300 s sobre o `t=` da tentativa). | Sincronize o NTP. | | `400` "id difere" | O seu código compara o `Dracma-Event-Id` com um campo errado, ou um proxy reescreveu o header. | O header é igual ao `id` do corpo. | | Redirect (`301`/`302`) | A URL cadastrada redireciona (`http` para `https`, barra final, `www`). | O Dracma não segue redirect: cadastre a URL final. | #### Chegou, mas o resultado está errado | Sintoma | Causa provável | O que fazer | |---|---|---| | O mesmo evento chega duas vezes | Esperado (at-least-once), ou o seu receptor demorou mais de 10 s e respondeu depois do corte. | Deduplique por `Dracma-Event-Id`; responda rápido e processe em fila. | | `deposit.paid` antes de `deposit.pending` | Esperado: não há ordem entre eventos. | Aja pelo estado da consulta e por `object_version`. | | O fetch-back devolve `404` | A chave de API é de outra operação (teste × real) ou o evento é forjado. | Use a chave da operação do evento (`operation_id`). Não credite. | | O `data.object` está "velho" numa reentrega | O corpo é congelado no momento da transição. | Use o estado da consulta. | | O jogador pagou, mas não houve webhook | Pode ter ido para a DLQ ou ficado `skipped`; o pagamento pode ter chegado depois da expiração. | Consulte o depósito e `GET /events`; veja [Catálogo de eventos](catalogo-de-eventos.md). | | O receptor responde 4xx de propósito para "rejeitar" | O Dracma repete 4xx por até 72 h. | Para um evento que você não quer tratar, responda 200. | #### Entregas na DLQ Um evento na DLQ esgotou 15 ou 16 tentativas em até 72 h. Corrija a causa, reentregue pelo painel (a reentrega mantém o `Dracma-Event-Id`) e confirme com o job de `GET /events` que não ficou lacuna. Veja [Retentativas, skipped e DLQ](retentativas-e-dlq.md#dlq-evento-que-esgotou-as-tentativas). ### Exemplos #### O que reunir antes de chamar o suporte Envie, no canal de suporte informado no onboarding: | Dado | De onde | |---|---| | `Dracma-Event-Id` do evento (`evt_…`) | Header ou campo `id` do corpo; log do receptor. | | `operation_id` | Campo do corpo; tela da operação no painel. | | Horário (UTC) e fuso da sua leitura | Log do receptor. | | Status HTTP e erro da última tentativa | Log de entregas do painel. | | O `object_id` (`dep_…` ou `wdr_…`) e o `external_id` | Corpo do evento. | | O que o seu receptor respondeu e o que ele fez | Log do receptor. | **Nunca envie** o segredo de webhook, o segredo de chamada ou a assinatura completa. Se precisar mostrar uma assinatura, mostre só os primeiros 8 caracteres do `v1`. #### Verificação mínima de um evento ```bash # Com o script assinar.sh dos exemplos executáveis ($DRACMA_BASE_URL inclui /v1): # 1. O evento existe para a sua operação? ./assinar.sh GET '/events?object_id=dep_0198b7c24d5e7a1b8c2d3e4f5a6b7c8d' # 2. Qual o estado atual do objeto? ./assinar.sh GET /deposits/dep_0198b7c24d5e7a1b8c2d3e4f5a6b7c8d ``` ### Erros e casos de borda - **Alerta de URL falhando:** o painel marca a saúde da URL como "falhando" quando a última tentativa não foi 2xx e a primeira falha desde o último 2xx tem pelo menos 15 minutos. - **Muitos erros ao mesmo tempo:** uma rajada de 4xx do seu receptor gera um volume grande de reentregas nas horas seguintes. Corrija o receptor primeiro; as tentativas pendentes vão se acomodando sozinhas. - **Teste no sandbox:** para provar o receptor, gere um depósito de teste, simule o pagamento e reentregue o evento; veja [Sandbox](../sandbox/README.md). - **Ainda sem causa:** abra um chamado no canal de suporte com os dados acima ([Suporte](../ajuda/suporte.md)). ### Veja também - [Receptor passo a passo](receptor.md) - [Retentativas, skipped e DLQ](retentativas-e-dlq.md) - [Segredo e rotação](segredos-e-rotacao.md) - [Problemas de integração](../ajuda/troubleshooting.md) - [Suporte](../ajuda/suporte.md) - [Exemplos executáveis](../exemplos/README.md) --- # Arquivo: docs/sandbox/README.md ## Sandbox > Para: Dev — Conferido em 2026-10-18. A operação **Teste** roda contra um simulador de PSP: nenhum dinheiro se move e você controla o desfecho de cada depósito e saque por rotas `/v1/sandbox/*`. Use-a para provar o seu conector inteiro antes de pedir uma operação Real. ### O que é O "sandbox" do Dracma é o comportamento de uma **operação do tipo Teste**. Ele não é um servidor separado: quem decide é a operação a que a chave pertence (prefixo `key_test_`). O `GET /v1/ping` devolve `environment: "sandbox"` quando a chave é de uma operação Teste. | | Operação Teste | Operação Real | |---|---|---| | Prefixo da chave | `key_test_` | `key_live_` | | Dinheiro | Nenhum se move | Dinheiro real, em qualquer instância | | PSP | Simulador do Dracma (sem conexão cadastrada, o simulador é o padrão) | PSP real da operação | | Rotas `/v1/sandbox/*` | Existem | Respondem `404 not_found`, sem executar nada | | `copy_paste` do PIX | BR Code sintético com o marcador `SANDBOX` | Cobrança real | Onde ela fica: no hml, o host `api.` atende operações Teste e o host `api-live.` atende operações Real. Em produção só existem operações Real. A tabela de hosts está em [Ambientes, tipos de operação e hosts](../conceitos/ambientes-e-hosts.md). - **PIX sintético.** O `instructions.pix.copy_paste` de um depósito Teste traz o marcador `SANDBOX` e uma chave de recebedor fictícia (`sandbox@dracma.invalid`). Nenhum banco aceita esse código. Se um `copy_paste` sem o marcador aparecer na sua operação Teste, pare e avise o suporte: você pode estar apontando para uma operação Real. - **Cada tipo recusa a chave do outro.** `key_test_` contra um host de operação Real (e o inverso) responde `401 unknown_key`. Veja [Ambientes, tipos de operação e hosts](../conceitos/ambientes-e-hosts.md). - **Mesma autenticação da produção** em todas as rotas, inclusive `/sandbox/*`: assinatura HMAC, nonce e janela de tempo ([Autenticação](../api/autenticacao.md)). - **Mesmos limites de taxa da produção.** O sandbox não tem um perfil de limites menor: o mesmo balde de listagem, o mesmo limite de depósitos e de saques por jogador por hora valem na operação Teste ([Limites e quotas](../api/limites.md)). Uma suíte que cria muitos saques para o mesmo jogador esbarra em `429 rate_limited`; use `external_ref` de jogadores diferentes. - **Webhook só para a URL cadastrada** na operação. O Dracma nunca entrega para uma URL que venha no corpo de uma requisição ([Webhooks](../webhooks/README.md)). !!! danger "Dinheiro real" "Sandbox" descreve o tipo da operação, não o hml. No hml existe também operação Real (`key_live_`), e ela move dinheiro real. Confira o prefixo da chave e o `environment` do `GET /v1/ping` antes de rodar uma receita desta seção. ### Quando usar - Antes de pedir a operação Real: o go-live exige a [Suíte de conformidade](conformidade.md) verde e as [Receitas por caminho](receitas.md) provadas contra o **seu** receptor e o **seu** livro. - Para testar um caminho que a produção só produz por acaso: pagamento de terceiro, valor divergente, expiração, estorno, chargeback, saque devolvido, queda do Dracma. - Para testar o seu código de repetição e de deduplicação: queda simulada e reentrega de webhook. ### Como funciona #### Rotas de controle | Rota | Escopo exigido | O que faz | |---|---|---| | `POST /v1/sandbox/deposits/{id}/pay` | `deposits:write` | Simula o pagamento do PIX; aceita pagador e valor diferentes | | `POST /v1/sandbox/deposits/{id}/expire` | `deposits:write` | `pending` vira `expired` | | `POST /v1/sandbox/deposits/{id}/refund` | `deposits:write` | `paid` vira `refunded` | | `POST /v1/sandbox/deposits/{id}/chargeback` | `deposits:write` | `paid` vira `chargeback` | | `POST /v1/sandbox/withdrawals/{id}/{action}` | `withdrawals:write` | Decisão do revisor ou resultado do PSP no saque (`approve`, `reject`, `pay`, `fail`, `return`) | | `POST /v1/sandbox/outage` | `deposits:write` ou `withdrawals:write` | Liga e desliga a queda simulada da operação | | `POST /v1/sandbox/webhooks/redeliver` | `deposits:write` ou `withdrawals:write` | Reentrega um evento já gravado | | `GET /v1/sandbox/webhooks` | `read` | Log das últimas entregas de webhook | - Sem o escopo, a resposta é `403 insufficient_scope`. Um objeto de **outra** operação responde `404 not_found`, nunca `403`. - Uma chave com `deposits:write`, `withdrawals:write` e `read` cobre todas as rotas da tabela. - Em operação Real, toda rota `/v1/sandbox/*` responde `404 not_found` a uma chave com o escopo exigido, sem executar nada. !!! note "Reservado no contrato" `POST /v1/sandbox/bank-transfers/incoming` existe só para transferência bancária, que a operação PIX não usa: um corpo válido recebe `400 validation_failed` com `reason: rail_not_enabled`. Veja [Rotas e campos reservados](../referencia/superficies-reservadas.md#transferencia-bancaria-simulada-post-v1sandboxbank-transfersincoming). #### Idempotência nas rotas de controle Mande `Idempotency-Key` em **todo** `POST`, inclusive nas rotas `/sandbox/*`; sem ela a API responde `400 validation_failed` com `field: Idempotency-Key`. Use uma chave **nova** a cada ação de controle (por exemplo `sbx_`). Repetir a mesma chave devolve a resposta guardada (`Idempotent-Replayed: true`) **sem executar a ação de novo**. Isso é o que você quer em `POST /v1/deposits`, mas atrapalha se você quiser, por exemplo, ligar e desligar a queda simulada duas vezes com a mesma chave. #### Pagar um depósito `POST /v1/sandbox/deposits/{id}/pay` aceita um corpo opcional. Vazio significa "pago pelo próprio jogador, valor exato". ```json { "payer_document": {"type": "CPF", "number": "11144477735", "country": "BR"}, "paid_amount": {"amount": "4999", "currency": "BRL"} } ``` Os dois campos são opcionais: `payer_document` (`type`, `number` e `country`) e `paid_amount` (um `Money`, na moeda do depósito). Se `payer_document` vier, `number` é obrigatório. O exemplo simula um terceiro pagando R$ 49,99, valor diferente do depósito. - Só funciona em depósito `pending`; em qualquer outro estado: `409 invalid_state_transition`. - `paid_amount` em moeda diferente da do depósito: `400 validation_failed` com `reason: currency_mismatch`. - A resposta é `200` com o objeto. A API espera até 3 segundos pelo resultado do simulador; se ele demorar mais, o objeto volta ainda em `pending` e o desfecho chega pelo webhook. Faça o fetch-back em vez de supor o estado. - Simulador indisponível: `503 service_unavailable` com `Retry-After: 1` e `retryable: true`. Repita com uma chave nova de controle. #### Ações no saque `POST /v1/sandbox/withdrawals/{id}/{action}`, sem corpo. A tabela completa de efeitos está em [Receitas por caminho](receitas.md). | `action` | Resposta quando aceita | |---|---| | `approve`, `reject` | `200` com o saque já decidido (como se o revisor tivesse agido no painel) | | `pay`, `fail` | `200` com o saque **no estado atual**; o desfecho (`paid` ou `failed`) chega depois, por webhook | | `return` | `200` com o saque já em `returned` | - Ação fora do enum: `400 validation_failed`. - Ação válida para outro estado: `409 invalid_state_transition`. - `compliance_hold`, `compliance_clear`, `compliance_reject`, `send`, `transit`, `recall_accept` e `recall_reject` existem no enum por causa de transferências bancárias; em PIX respondem `409 invalid_state_transition`. - Simulador indisponível: `503 service_unavailable` com `Retry-After: 5`. #### Queda simulada `POST /v1/sandbox/outage` com `{"down": true}` responde `{"down": true}`. A partir daí, toda rota da sua operação fora de `/v1/sandbox/*` responde `503 service_unavailable`, com `retryable: true` e `Retry-After: 5`. `GET /v1/ping` e as próprias rotas de sandbox continuam respondendo. Outras operações não são afetadas. `{"down": false}` religa. O campo `down` é obrigatório: sem ele, `400 validation_failed`. ```mermaid sequenceDiagram participant C as Seu conector participant D as Dracma (operação Teste) C->>D: POST /v1/sandbox/outage {"down": true} D-->>C: 200 {"down": true} C->>D: POST /v1/deposits (Idempotency-Key K) D-->>C: 503 service_unavailable, Retry-After 5 C->>D: POST /v1/sandbox/outage {"down": false} (chave nova) D-->>C: 200 {"down": false} C->>D: POST /v1/deposits (mesma chave K, mesmo corpo) D-->>C: 201 ou 202 C->>D: POST /v1/deposits (mesma chave K, mesmo corpo) D-->>C: mesma resposta, Idempotent-Replayed true ``` #### Log de entregas e reentrega de webhook `GET /v1/sandbox/webhooks` devolve `{"data": [...]}` com as **50 entregas mais recentes** da operação, da mais nova para a mais antiga. Não há filtro nem paginação, e o corpo do evento não vem no log. Cada item traz: | Campo | Conteúdo | |---|---| | `id` | Id da entrega | | `event_id`, `event_type` | O evento entregue (`Dracma-Event-Id`) e o seu tipo | | `environment` | `sandbox` para operação Teste | | `status` | `pending`, `delivered`, `dlq` ou `skipped` | | `skip_reason` | Presente só em `skipped` | | `attempts`, `total_attempts` | Tentativas do ciclo atual e acumuladas | | `next_attempt_at` | Próxima tentativa, enquanto `pending` | | `last_status_code`, `last_duration_ms` | Resposta HTTP do seu receptor e a duração | | `last_error` | Erro de rede ou de TLS, truncado em 200 caracteres | | `created_at`, `delivered_at`, `resolved_at`, `resolution_reason` | Marcas de tempo e motivo de resolução | Use esse log para ver as suas respostas `401` e `5xx` sem precisar de log no receptor. Estados e cronograma: [Retentativas, skipped e DLQ](../webhooks/retentativas-e-dlq.md). `POST /v1/sandbox/webhooks/redeliver` com `{"event_id": "evt_..."}` reentrega: - `200` com `{"event_id": "evt_...", "scheduled": n}`, onde `n` é o número de entregas reabertas (uma por URL cadastrada). Cada entrega reaberta volta a `pending` com as tentativas zeradas. - Funciona também para um evento que já estava `delivered`, o que permite testar a sua deduplicação. A reentrega leva o **mesmo** corpo e o **mesmo** `Dracma-Event-Id`. - `event_id` fora do formato `evt_` mais letras e dígitos: `400 validation_failed` com `reason: format`. - Evento inexistente ou de outra operação: `404 not_found`. ### Exemplos As chamadas abaixo usam o cliente de shell de [Exemplos executáveis](../exemplos/README.md). Os comandos completos, do ping ao saque, estão em [Primeiro depósito e primeiro saque](../onboarding/quickstart-sandbox.md). ```bash # Ligar a queda simulada (chave de controle nova a cada chamada) echo '{"down":true}' > corpo-queda.json bash exemplos/shell/assinar.sh POST /sandbox/outage corpo-queda.json "sbx_$(date +%s)_ligar" # Ver as últimas entregas de webhook bash exemplos/shell/assinar.sh GET /sandbox/webhooks ``` ### Erros e casos de borda | Você vê | Causa | O que fazer | |---|---|---| | `404 not_found` em toda rota `/sandbox/*` | A chave é de operação Real, ou o objeto é de outra operação | Confira o prefixo da chave e o `environment` do `GET /v1/ping` | | `403 insufficient_scope` | A chave não tem o escopo da tabela | Peça uma chave com o escopo certo | | `409 invalid_state_transition` | O objeto não está no estado que a ação exige | Leia o objeto com `GET` e escolha a ação certa | | `400 validation_failed` com `Idempotency-Key` | Faltou a chave no `POST` | Mande uma chave nova | | `503 service_unavailable` em rotas normais | Queda simulada ligada | Desligue com `{"down": false}` ou trate como no [Retry](../exemplos/README.md) | | `503 service_unavailable` em `/sandbox/*` | Simulador do PSP indisponível | Repita com `Retry-After` e chave nova | | `429 rate_limited` | Limites iguais aos da produção | Espere o `Retry-After` | ### Veja também - [Receitas por caminho](receitas.md): um passo a passo para cada desfecho. - [Suíte de conformidade](conformidade.md): o teste de aceite do contrato. - [Dados de teste](dados-de-teste.md): CPF, e-mail e valores que o simulador reconhece. - [Ciclo do depósito](../dinheiro/ciclo-do-deposito.md) e [Ciclo do saque](../dinheiro/ciclo-do-saque.md): as máquinas de estado. - [Webhooks](../webhooks/README.md): garantias de entrega e como verificar. --- # Arquivo: docs/sandbox/receitas.md ## Receitas por caminho > Para: Dev — Conferido em 2026-10-18. Doze receitas para provar, na operação Teste, cada caminho que o seu conector precisa tratar. Cada uma diz o que chamar, o que esperar e o que o **seu** livro deve fazer. ### O que é Uma receita é um roteiro curto: crie o objeto, dispare a ação de sandbox, espere o webhook, faça o fetch-back e confira o efeito no seu livro. Todas usam as rotas descritas em [Sandbox](README.md). Regras que valem para todas: - `POST` significa requisição assinada **com** `Idempotency-Key`. Em ações de sandbox use uma chave nova a cada ação. - Entre um passo e o seguinte, espere o webhook ou leia o objeto (fetch-back). Nunca assuma o estado pelo que você acabou de chamar. - Os corpos completos de criação estão em [Depósitos](../referencia/depositos.md) e [Saques](../referencia/saques.md). - Cada jogador pode pedir poucos saques por hora, também no sandbox ([Limites e quotas](../api/limites.md#limites-por-jogador)). Para rodar várias receitas seguidas, use um `external_ref` de jogador diferente em cada uma. - Para sacar, a operação precisa de caixa, e o caixa só cresce com depósitos pagos. Antes de qualquer receita de saque, pague um depósito (receita 1). Sem caixa livre, a criação do saque responde `422 insufficient_operation_balance`. ### Quando usar Rode todas antes do go-live e depois de cada mudança no seu receptor ou no seu livro. Em cada uma, confira três coisas: **1 crédito** por depósito pago, **0 crédito** quando o webhook é forjado ou reentregue, e **reserva devolvida** em `rejected`, `failed` e `cancelled`. ### Como funciona #### Ação, evento e versão Cada transição de estado grava **um** evento e sobe `object_version` em 1. Use esta tabela para saber o que o seu receptor vai ver. | O que você faz | Estado resultante | Evento | `object_version` | |---|---|---|---| | `POST /v1/deposits` | `pending` (o objeto nasce `created`, que não gera evento) | `deposit.pending` | 2 | | `.../pay` exato, pagador igual ao jogador | `paid` | `deposit.paid` | 3 | | `.../pay` com outro pagador ou outro valor | `under_review` | `deposit.under_review` | 3 | | `.../expire` | `expired` | `deposit.expired` | 3 | | `POST /v1/deposits/{id}/cancel` | `cancelled` | `deposit.cancelled` | 3 | | `.../refund` (de `paid`) | `refunded` | `deposit.refunded` | 4 | | `.../chargeback` (de `paid`) | `chargeback` | `deposit.chargeback` | 4 | | `POST /v1/withdrawals` automático | `approved` | `withdrawal.approved` | 1 | | `POST /v1/withdrawals` com revisão | `under_review` | `withdrawal.under_review` | 1 | | `.../approve` | `approved` | `withdrawal.approved` | +1 | | `.../reject` | `rejected` | `withdrawal.rejected` | +1 | | Simulador envia o PIX (`.../pay` ou automático) | `processing` | `withdrawal.processing` | +1 | | Simulador liquida | `paid` | `withdrawal.paid` | +1 | | `.../fail` | `processing`, depois `failed` | `withdrawal.processing`, `withdrawal.failed` | +1, +1 | | `.../return` (de `paid`) | `returned` | `withdrawal.returned` | +1 | - No saque, a versão do primeiro evento é 1, **exceto** quando a chave PIX precisa de consulta ao DICT: o saque nasce `requested` (estado que não aparece em `GET /v1/events` nem em webhook), e a primeira versão visível é 2. - Compare sempre a versão do evento com a que você já aplicou, não com um número fixo. Eventos podem chegar fora de ordem: aplique só a versão maior. - O catálogo completo, com payloads, está em [Catálogo de eventos](../webhooks/catalogo-de-eventos.md). ```mermaid stateDiagram-v2 direction LR [*] --> pending: POST /deposits pending --> paid: pay exato pending --> under_review: pay de terceiro ou valor diferente pending --> expired: expire pending --> cancelled: cancel paid --> refunded: refund paid --> chargeback: chargeback ``` #### Valor terminado em 77: saque que espera por você Por padrão, o simulador **paga o saque sozinho** assim que ele é despachado: `approved`, `processing` e `paid` chegam em poucos segundos. Isso é útil para o caminho feliz, mas não deixa tempo para você agir. Para controlar o desfecho, crie o saque com um valor cujos **dois últimos dígitos na menor unidade são `77`** (em BRL, centavos `,77`; por exemplo `"2077"`, R$ 20,77). O saque fica em `processing` e só muda quando você chamar `pay`, `fail` ou `return`. Sem esse gatilho, uma chamada a `.../pay` pode correr contra o pagamento automático e receber `409 invalid_state_transition`. | Quero testar | Crie o saque com | Depois chame | |---|---|---| | Pagamento (receita 5) | qualquer valor | nada; o simulador paga | | Pagamento explícito | valor `xx77` | `pay` | | Falha do PSP e devolução da reserva | valor `xx77` | `fail` | | Devolução depois de pago | qualquer valor | espere `paid` e chame `return` | O gatilho `77` só existe no simulador. Ele não afeta nenhuma operação Real. ### Exemplos Os exemplos de shell supõem `dr` como no [Primeiro depósito e primeiro saque](../onboarding/quickstart-sandbox.md) e as variáveis `DEP_ID`, `DEP_EXT`, `WDR_ID`. #### Receita 1: depósito pago pelo próprio jogador, `paid` 1. `POST /v1/deposits` com `player.document` igual ao CPF do jogador. A resposta é `201` com `pending` e `instructions.pix`, ou `202` com `created`, que passa a `pending` em seguida. 2. `POST /v1/sandbox/deposits/{id}/pay` com corpo vazio. Resposta `200` com o objeto. 3. Chega o webhook `deposit.paid`. Faça `GET /v1/deposits/{id}` e confira `status: paid`. **Credite uma vez**, por `external_id`. 4. Confira que o evento aparece em `GET /v1/events?object_id={id}`. ```bash dr POST "/sandbox/deposits/$DEP_ID/pay" "" "sbx_$(novo_id)" dr GET "/deposits/$DEP_ID" dr GET "/events?object_id=$DEP_ID" ``` #### Receita 2: pago por terceiro, `under_review` com `PAYER_MISMATCH` 1. Crie o depósito (receita 1, passo 1). 2. Chame `pay` com um `payer_document` **diferente** do documento do jogador, por exemplo outro CPF de teste. 3. Resultado: `under_review`, com `review_reasons` contendo `PAYER_MISMATCH`. **Nada é creditado** e o webhook é `deposit.under_review`. 4. A decisão (aprovar ou devolver) é de um revisor no painel ([Revisão de depósitos e saques](../painel/revisao-de-depositos-e-saques.md)). Aprovado, o depósito vai a `paid` e aí você credita; devolvido, vai a `returned` e você não credita. ```bash echo '{"payer_document":{"type":"CPF","number":"11144477735","country":"BR"}}' > pagador.json dr POST "/sandbox/deposits/$DEP_ID/pay" pagador.json "sbx_$(novo_id)" ``` #### Receita 3: valor divergente, `under_review` com `AMOUNT_MISMATCH` 1. Crie um depósito de `5000`. 2. Chame `pay` com `{"paid_amount": {"amount": "4999", "currency": "BRL"}}`. Moeda diferente da do depósito: `400 validation_failed` com `reason: currency_mismatch`. 3. Resultado: `under_review` com `AMOUNT_MISMATCH`. **Nada é creditado** e o webhook `deposit.paid` **não** é emitido para esse depósito. #### Receita 4: expiração, `expired` 1. Crie o depósito e espere `pending`. 2. `POST /v1/sandbox/deposits/{id}/expire`: `expired`, com o webhook `deposit.expired`. `expire` só vale a partir de `pending`; fora disso, `409 invalid_state_transition`. 3. Chame `pay` depois: **não** leva a `paid`. 4. `POST /v1/deposits/{id}/cancel`: `409 invalid_state_transition`, porque `expired` é terminal. #### Receita 5: saque automático, `paid` 1. Tenha um jogador com um depósito `paid` (receita 1). A regra X× só conta o que o Dracma liquidou. 2. **Reserve** o valor no seu livro e chame `POST /v1/withdrawals` com valor dentro do limite e `review_requested: false`. Mande `player.ip`: sem ele o saque nunca é automático (`OWNERSHIP_UNVERIFIED`). 3. A resposta é `201` com `approved` e `decision.mode: automatic`. O simulador paga sozinho: chegam `withdrawal.approved`, `withdrawal.processing` e `withdrawal.paid`. Faça o fetch-back e **finalize a reserva** ao ler `paid`. !!! note "O primeiro saque pode ir para revisão" Dependendo das regras da operação (veja `GET /v1/rules`), o primeiro saque de um jogador vai para revisão com `FIRST_WITHDRAWAL`. Leia `decision.reasons`. Para chegar ao caminho automático, faça antes um primeiro saque e aprove-o (receita 8). #### Receita 6: saque acima do limite, `under_review` com `AUTO_MULTIPLIER_EXCEEDED` Peça um saque maior que o total depositado e liquidado vezes o multiplicador da operação. Resultado: `under_review`, `decision.mode: manual` e `decision.reasons` com `AUTO_MULTIPLIER_EXCEEDED` (ou `NO_SETTLED_DEPOSIT`, se o jogador nunca teve depósito liquidado). A reserva continua no seu livro. #### Receita 7: `review_requested: true` nunca é automático Repita a receita 5 com `"review_requested": true`. Resultado: `under_review` com `OPERATOR_REVIEW_REQUESTED` em `decision.reasons`, mesmo que as regras liberassem o automático. O operador só consegue tornar a decisão **mais** restritiva; nunca pular uma revisão. #### Receita 8: decisões e resultado do PSP no saque `POST /v1/sandbox/withdrawals/{id}/{action}`: | `action` | De, para | O que o seu livro faz | |---|---|---| | `approve` | `under_review` para `approved` (o Dracma já despacha) | Nada. A reserva continua. | | `reject` | `requested` ou `under_review` para `rejected` | **Devolve a reserva.** | | `pay` | `approved` ou `processing` para `paid` (assíncrono) | Finaliza a saída ao ler `paid`. | | `fail` | `approved` ou `processing` para `failed` (assíncrono) | **Devolve a reserva** ao ler `failed`. O objeto traz `failure_code`. | | `return` | `paid` para `returned` (síncrono) | A política é sua. Recomendação: nada automático; abra uma decisão manual e dispare um alerta. | Para `pay` e `fail` com tempo de agir, use o valor terminado em `77` (seção acima). `approve` só vale em `under_review`; ação fora da máquina de estados responde `409 invalid_state_transition`. O cancelamento pelo operador é `POST /v1/withdrawals/{id}/cancel`: funciona em `requested`, `under_review` e em `approved` antes de o envio sair; depois disso, `409`. !!! warning "Chave PIX que não é CPF ou CNPJ" No simulador, o titular de uma chave `email`, `phone` ou `random` é um CPF sintético estável, derivado da chave, e não o do jogador. Com uma chave dessas, o saque tende a voltar `422 document_mismatch` ou `rejected`. Para o caminho feliz use `key_type: cpf` com o CPF do próprio jogador. Para testar a recusa por titularidade, use uma chave `cpf` de outra pessoa. #### Receita 9: queda e retomada sem duplicar 1. `POST /v1/sandbox/outage` com `{"down": true}`. Daí em diante, toda rota da operação fora de `/sandbox/*` responde `503` (`service_unavailable`, `retryable: true`, `Retry-After: 5`). `GET /v1/ping` continua respondendo. 2. `POST /v1/deposits` com `Idempotency-Key: K`: `503`. 3. `POST /v1/sandbox/outage` com `{"down": false}` e uma chave de controle nova. 4. Repita o passo 2 com a **mesma** `K` e o **mesmo** corpo: `201` ou `202`. Repita mais uma vez: a mesma resposta, agora com `Idempotent-Replayed: true`. 5. `GET /v1/deposits?external_id=...`: exatamente **1** depósito. É esse o comportamento que o seu cliente precisa ter em produção: diante de `5xx`, `429` ou erro de rede, repita com a **mesma** chave. Há um helper pronto em [Exemplos executáveis](../exemplos/README.md). #### Receita 10: reentrega de webhook, teste da sua deduplicação 1. Rode uma receita que gere webhook (a 1) e anote o `Dracma-Event-Id`. 2. `POST /v1/sandbox/webhooks/redeliver` com `{"event_id": "evt_..."}`: `200` com `scheduled`. 3. O seu receptor recebe o **mesmo** `Dracma-Event-Id` e responde `200` **sem efeito**. O crédito continua sendo **1**. 4. `GET /v1/sandbox/webhooks` mostra a entrega e o status HTTP que você devolveu. #### Receita 11: webhook forjado não cria dinheiro Assine você mesmo um `deposit.paid` com o seu segredo de webhook, apontando para um depósito que **não está pago**, e envie ao seu receptor. A assinatura é válida, mas o fetch-back devolve `pending`. Resultado esperado: `200` e **nenhum crédito**. Veja [Receptor passo a passo](../webhooks/receptor.md#fetch-back-por-que-e-obrigatorio). #### Receita 12: estorno e chargeback depois de `paid` 1. Tenha um depósito `paid` já creditado no seu livro (receita 1). 2. `POST /v1/sandbox/deposits/{id}/refund` ou `.../chargeback`, com corpo vazio ou `{}`. A resposta é `200` com o objeto em `refunded` ou `chargeback` e `version` maior. Fora de `paid`: `409 invalid_state_transition`. Campo no corpo: `400`. 3. Chega `deposit.refunded` ou `deposit.chargeback`. Faça o fetch-back e aplique a **sua política de débito**. Recomendação: abra uma decisão manual e dispare um alerta; nunca debite sozinho ([Política de exceções](../conector/politica-de-excecoes.md#fluxo-quando-mover-dinheiro-sozinho)). !!! warning "Só existe no sandbox" Estorno e chargeback são gatilhos de simulação. Em produção não há um gatilho equivalente hoje: o seu conector precisa tratar os dois estados, mas não os verá acontecer. Veja [Devoluções, estornos e disputas](../dinheiro/devolucoes-estornos-e-disputas.md). ### Erros e casos de borda | Sintoma | Causa provável | O que fazer | |---|---|---| | `pay` em saque devolve `409 invalid_state_transition` | O simulador já pagou sozinho | Crie o saque com valor `xx77` | | `422 insufficient_operation_balance` ao criar saque | Caixa da operação sem saldo livre | Pague antes um depósito (receita 1) | | `429 rate_limited` ao criar saque | Muitos saques do mesmo jogador na hora | Use outro `external_ref` ou espere o `Retry-After` | | Saque sempre `under_review` | Falta `player.ip`, primeiro saque ou chave nova | Leia `decision.reasons` | | `422 document_mismatch` no saque | Chave PIX de outro titular, ou chave não-CPF no simulador | Use chave `cpf` do próprio jogador | | Depósito `created` por muito tempo | A cobrança ainda está sendo criada | Espere o webhook `deposit.pending` ou leia o objeto | ### Veja também - [Sandbox](README.md): rotas de controle, escopos e erros. - [Suíte de conformidade](conformidade.md): as mesmas receitas, automatizadas. - [Ciclo do depósito](../dinheiro/ciclo-do-deposito.md) e [Ciclo do saque](../dinheiro/ciclo-do-saque.md). - [Retentativas, skipped e DLQ](../webhooks/retentativas-e-dlq.md). --- # Arquivo: docs/sandbox/conformidade.md ## Suíte de conformidade > Para: Dev — Conferido em 2026-10-18. A suíte de conformidade é o teste de aceite do contrato da API v1. É um programa em Go que fala só HTTP com a sua operação Teste e traz um operador de referência para receber os webhooks. Verde é pré-requisito do go-live. ### O que é - É **caixa-preta**: usa a superfície pública (`/v1/*` e `/v1/sandbox/*`) e um listener local para os webhooks. Não lê banco nem código do servidor. - Traz um **operador de referência** que recebe o webhook, verifica a assinatura, deduplica, faz o fetch-back e registra o crédito num livro próprio, só na memória da suíte. - Ela testa o **Dracma**, usando o operador de referência como contraparte. Ela **não** testa o seu conector. Para provar o seu lado, rode as [Receitas por caminho](receitas.md) contra o **seu** receptor e o **seu** livro. - Um caso que precisa de uma configuração que você não passou **falha** com "configuração ausente"; ele nunca é pulado como verde. - A suíte é entregue pelo Dracma no onboarding, junto com as credenciais da operação Teste. ### Quando usar - Uma vez no onboarding, antes de pedir a operação Real, e guardando a saída como prova do go-live ([Checklist de go-live](../go-live/checklist.md)). - De novo quando o Dracma publicar uma versão nova do contrato ou do `openapi.yaml` ([Changelog](../versionamento/changelog.md)). - Como teste de regressão do seu ambiente: relógio, rede até o Dracma e URL de webhook alcançável. ### Como funciona #### O que ela prova Casos obrigatórios do contrato (PIX). Cada um já foi visto falhar quando a regra correspondente é quebrada. | # | O que prova | Caso | |---|---|---| | 1 | Assinatura inválida, timestamp fora da janela e nonce repetido são recusados (`401` com o `code` certo) e nada é criado | `TestInvalidSignatureRejected`, `TestTimestampOutOfWindowRejected`, `TestReplayedNonceRejected`, `TestT1UnknownKeyAndMissingHeaders` | | 2 | Mesma chave e mesmo corpo devolvem a mesma resposta com `Idempotent-Replayed: true`; corpo diferente dá `409 idempotency_key_reused` | `TestIdempotencySameResponse`, `TestIdempotencyDifferentBody409` | | 3 | Depósito, `pay`, webhook `deposit.paid` assinado e fetch-back coerente geram **exatamente 1** crédito | `TestDepositPaidWebhookAndFetchBack`, `TestE2EP001DepositPaid` | | 4 | Webhook reentregue com o mesmo `Dracma-Event-Id` responde `200` sem efeito | `TestWebhookRedeliveredSameEventID` | | 5 | `expired` é terminal: pagar depois não vira `paid` e cancelar dá `409` | `TestDepositExpiration` | | 6 | Saque dentro do limite chega a `approved` (automático) e a `paid` | `TestWithdrawalWithinMultiplierPaid`, `TestE2EP002WithdrawalPaid`, `TestSandboxWithdrawalPayExplicit` | | 7 | Saque acima do limite vai a `under_review` com `AUTO_MULTIPLIER_EXCEEDED` | `TestWithdrawalAboveMultiplierUnderReview` | | 8 | `review_requested: true` nunca é automático | `TestReviewRequestedNeverAutomatic` | | 9 | Objeto de outra operação responde `404` em todas as rotas | `TestCrossOperationObject404`, `TestT4BOLA`, `TestDoD6CrossTenantEveryRoute` | | 10 | `/sandbox/outage` dá `503` repetível, e a retomada com a mesma chave não duplica | `TestSandboxOutageRetrySameKey` | Casos adicionais: | O que prova | Caso | |---|---| | Webhook forjado, com corpo alterado, timestamp velho ou `t=` divergente não credita; nem um `deposit.paid` válido para um depósito que a API diz `pending` | `TestT3ForgedWebhooksNoCredit` | | Valor divergente vai a `under_review` com `AMOUNT_MISMATCH` e não emite `deposit.paid` | `TestE2EP001AmountMismatchUnderReview` | | Chave PIX de e-mail passa pela consulta de titularidade do simulador | `TestE2EC123DictEmailKey` | | Máquina de estados: transição inválida dá `409 invalid_state_transition` e não muda o estado | `TestT14StateMachine` | | Campo proibido no corpo (`operation_id`, `status`, `decision`, `version`, `paid_amount`) é recusado ou ignorado, nunca aplicado | `TestT9MassAssignment` | | Corpos hostis (SQL, número em vez de texto, notação científica, UTF-8 inválido, corpo acima de 64 KiB) dão `400` ou `413`, nunca `5xx` | `TestT13InjectionAndCoercion` | | A chave de integração não muda regras, webhook, chave nem IP; um pedido de regra fica `pending_approval` e não vigora | `TestT8OperatorCannotChangeControlPlane` | | 50 saques simultâneos com a mesma chave criam 1 objeto; dois saques simultâneos não passam do limite automático | `TestT5ConcurrentIdempotentWithdrawals`, `TestT6DoubleWithdrawalRace` | | Digest e totais da reconciliação batem com o recalculado de forma independente | `TestC253ReconciliationDigestIndependent` | Casos de segurança que precisam de configuração extra (flags da próxima seção): chave de Real contra Teste e o inverso, rotas `/sandbox/*` em Real, revogação de chave, segredo ausente de log e resposta, rastreio por `trace_id`, limites de taxa e cadastro de URL de webhook. !!! warning "Não disponível" O caso `TestT17IPAllowlist`, que espera `403 ip_not_allowed` para um IP fora da lista, depende de a lista de IPs ser aplicada às chamadas. Hoje a lista fica registrada mas não é aplicada, então esse caso não se aplica: não passe `-blocked-ip-key-id`. Veja [IPs do conector](../painel/allowlist-de-ip.md). #### Flags | Flag | Conteúdo | |---|---| | `-dracma-url` | Base da API da operação Teste, **incluindo** `/v1` | | `-key-id` e `-key-secret-file` | Chave `key_test_` da operação e o arquivo com o segredo. Nunca passe o segredo na linha de comando | | `-webhook-secret-file` | Segredos de webhook ativos, **um por linha** (1 ou 2 durante a rotação) | | `-webhook-listen` | Endereço do listener local do operador de referência (padrão: o endereço local da máquina, em uma porta livre) | | `-webhook-url` | URL cadastrada na operação que chega nesse listener (padrão `http:///webhook`) | | `-currency` | Moeda da carteira (padrão `BRL`) | | `-wait` e `-webhook-wait` | Espera máxima por uma transição (padrão 30 s) e por um webhook (padrão 90 s) | | `-quiesce-timeout` | Quanto esperar, antes dos casos, que os limites de taxa de uma rodada anterior liberem (padrão 5 min) | | `-key2-id` e `-key2-secret-file` | Chave de uma operação de **outro** tenant, para os casos de isolamento | | `-live-url`, `-live-key-id`, `-live-key-secret-file` | Operação Real, para a recusa cruzada de chaves e o `404` das rotas de sandbox | | `-blocked-ip-key-id`, `-blocked-ip-key-secret-file` | Chave cuja lista de IPs não inclui o IP da suíte (não se aplica hoje, veja o aviso acima) | | `-revoke-key-id`, `-revoke-key-secret-file`, `-revoke-cmd`, `-revoke-deadline` | Revogação de chave, com o comando de revogação e o prazo (padrão 5 min) | | `-log-file`, `-live-log-file` | Log JSON do servidor, para os casos de vazamento de segredo e rastreio | | `-recon-date` | Dia UTC já fechado para o caso de reconciliação (padrão: o dia fechado mais recente) | | `-openapi` | `openapi.yaml` de onde as rotas do caso de isolamento são enumeradas | Os arquivos de segredo ficam fora do repositório, com permissão `0400`, entregues pelo seu cofre. ### Exemplos Execução do conjunto base, que só precisa da sua operação Teste e do seu receptor: ```bash cd conformance go test ./conformance -count=1 -v \ -run 'TestInvalidSignature|TestTimestampOutOfWindow|TestReplayedNonce|TestT1UnknownKey|TestIdempotency|TestDeposit|TestWebhookRedelivered|TestWithdrawal|TestReviewRequested|TestSandbox|TestE2E|TestT3|TestT9|TestT13|TestT14' \ -dracma-url="$DRACMA_BASE_URL" \ -key-id=key_test_SUAOPERACAO \ -key-secret-file=/caminho/seguro/segredo-chamada \ -webhook-secret-file=/caminho/seguro/segredos-webhook \ -webhook-listen=localhost:0 \ -webhook-url=https://pagamentos.operador.example/dracma/webhook \ -currency=BRL ``` - O listener do operador de referência precisa ser alcançável pelo Dracma na `-webhook-url` cadastrada na operação. Se o Dracma não consegue chegar nele, os casos de webhook falham por tempo esgotado em `-webhook-wait`. - Para a execução completa, acrescente as flags opcionais e tire o `-run`. A execução completa inclui o caso de limites de taxa, que **consome** os baldes da chave e do IP e roda por último; ele pode deixar o IP da suíte limitado por alguns minutos. Rodadas seguidas esperam esse bloqueio passar antes de começar. - Há ainda os testes `TestOffline*`, que não precisam de servidor: eles conferem vetores de assinatura, o verificador e o operador de referência. ### Erros e casos de borda | Sintoma | Causa provável | O que fazer | |---|---|---| | `configuração ausente: -key2-id` | O caso exige uma segunda operação ou a operação Real, que você não passou | Rode com `-run` no conjunto base, ou passe as flags do caso | | Casos de saque caem em revisão em vez de automático | As regras da operação foram mudadas (janela, horário, multiplicador) | Os casos de saque assumem as regras iniciais; restaure-as ou peça a restauração ao suporte | | Timeout esperando webhook | A URL cadastrada não chega ao listener local, ou o relógio está desviado | Confira a URL, o firewall e o NTP | | `429 rate_limited` em sequência | Duas rodadas seguidas, ou o caso de limites de taxa rodou antes | Espere ou aumente `-quiesce-timeout` | | Falhas só em segurança com `-live-*` | A chave de Real aponta para o host errado | Confira o host de Real e o prefixo `key_live_` | !!! warning "Operação Real" Passe `-live-*` somente com uma chave de Real quando o Dracma pedir. Os casos que usam essa chave só tentam requisições recusadas, mas a chave é de dinheiro real: guarde-a como qualquer outra credencial de produção. ### Veja também - [Sandbox](README.md) e [Receitas por caminho](receitas.md). - [Checklist de go-live](../go-live/checklist.md): a suíte verde é um dos itens. - [Autenticação](../api/autenticacao.md#ordem-de-verificacao): a ordem de verificação que os casos de assinatura exercitam. - [Exemplos executáveis](../exemplos/README.md): os vetores de assinatura que a suíte também confere. --- # Arquivo: docs/sandbox/dados-de-teste.md ## Dados de teste > Para: Dev — Conferido em 2026-10-18. Que CPF, e-mail, chave PIX e valores usar na operação Teste, e o que nunca usar. Nada aqui é de uma pessoa real, e nada chega a ninguém. ### O que é Os dados de teste têm três regras: 1. **Documento**: um CPF gerado, com os dois dígitos verificadores válidos e sem titular real conhecido. 2. **E-mail e domínios**: só domínios reservados (`.test`, `.invalid`, `.example`). É a API que impõe: numa operação Teste, um `player.email` fora desses domínios responde `400 validation_failed` com `reason: reserved_domain_required`. 3. **Valores**: inteiros na menor unidade, como em produção. Só o valor terminado em `77` tem significado especial no simulador (saque que espera por você, em [Receitas por caminho](receitas.md#valor-terminado-em-77-saque-que-espera-por-voce)). !!! danger "Nunca use dados de uma pessoa real" Nem o seu CPF, nem o seu e-mail, nem o de um cliente. Um e-mail de pessoa real em dados de teste é um risco: dado pessoal em ambiente de teste e, se algum dia a operação for ligada a um destino de verdade, e-mail enviado a quem não pediu. O domínio de produção do seu produto também não entra em fixture. ### Quando usar Em fixtures, seeds de teste, testes de carga e na demonstração para o time. Vale igualmente para o hml (operação Teste) e para a sua máquina de desenvolvimento. ### Como funciona #### CPF de teste O Dracma valida o documento do jogador pelo módulo 11. Um CPF é recusado com `422 document_invalid` quando: - não tem exatamente 11 dígitos; - tem todos os dígitos iguais (`11111111111`); - um dos dois dígitos verificadores não confere. Para gerar um CPF válido de teste, use o script `exemplos/shell/cpf-teste.sh`, que também tem teste: ```bash bash exemplos/shell/cpf-teste.sh 3 # imprime 3 CPFs de teste, só dígitos ``` O algoritmo, para você portar à sua linguagem: 1. Sorteie 9 dígitos. 2. Primeiro dígito verificador: some cada dígito multiplicado por `10, 9, 8, 7, 6, 5, 4, 3, 2`; seja `r` o resto dessa soma por 11. O dígito é `0` se `r < 2`, senão `11 - r`. 3. Segundo dígito verificador: repita sobre os 10 dígitos, com pesos `11, 10, 9, ..., 2`. 4. Descarte o resultado se os 11 dígitos forem todos iguais. Estes CPFs são válidos nas receitas e nos exemplos: `52998224725` e `11144477735`. Eles passam no dígito verificador, mas são conhecidos e públicos: use-os só como dado fictício, nunca como o documento de alguém. Dois pontos de atenção: - O documento do jogador é fixado no primeiro uso de um `external_ref`: um CPF diferente depois dá `422 document_mismatch`. Em teste, use um `external_ref` novo para cada CPF novo. - Na chave PIX do saque, `key_type: cpf` exige o mesmo CPF do jogador. Veja a [Receita 8](receitas.md#receita-8-decisoes-e-resultado-do-psp-no-saque). #### E-mail e outros dados do jogador - `player.email`: `local@dominio.tld` com até 254 caracteres e domínio terminado em `.test`, `.invalid` ou `.example`. Por exemplo `jogador1@operador.test`. - `player.name`: até 120 caracteres. Use nomes obviamente fictícios. - `player.ip`: endereços de documentação (`203.0.113.0/24`, `198.51.100.0/24`, `2001:db8::/32`). Eles servem para a regra de titularidade do saque; sem `player.ip`, o saque nunca é automático. - `player.external_ref`: o seu id interno, imutável e opaco. Nunca e-mail, telefone ou CPF. - Quais campos a operação exige para criar depósito: `GET /v1/currencies` (`deposit_requirements`). Veja [Ping e moedas](../referencia/ping-e-moedas.md). - URL de webhook: `https://pagamentos.operador.example/dracma/webhook`. O Dracma entrega fora de produção apenas a domínios reservados ou liberados para você no onboarding ([Seu endpoint](../webhooks/endpoint-e-politica-de-url.md)). Numa operação **Real** (inclusive no hml) a regra do e-mail é a de produção, sem restrição de domínio. Isso não é licença para usar e-mail de verdade em teste: use a operação Teste. #### E-mail real no hml: painel e convites O e-mail que **não** é reservado também existe no hml: o do painel. No hml e em produção, o código de acesso e o convite do administrador da sua empresa são entregues de verdade, por e-mail. Por isso: - Informe, no onboarding, o e-mail real de quem vai administrar. É para esse endereço que o convite chega ([Conta, login e dispositivos](../painel/conta-e-login.md)). - Não use um endereço fictício (`.test`, `.invalid`, `.example`) para convidar uma pessoa do painel: o convite não chegaria a ninguém. - Não misture os dois mundos. O e-mail do painel é de gente. O `player.email` da API é sempre fictício. !!! note "Confirmar após deploy" O convite por e-mail ao administrador é o fluxo de primeiro acesso, e a sua ativação na instância ainda precisa ser confirmada depois do deploy. Antes de combinar a data do onboarding, confirme com o suporte que o convite já está ativo. #### Valores - Dinheiro é sempre string de inteiro na menor unidade: `"5000"` é R$ 50,00. Nunca use ponto flutuante ([Convenções HTTP](../conceitos/convencoes-http.md)). - O mínimo e o máximo por depósito e por saque vêm das regras da operação (`GET /v1/rules`). Valores abaixo ou acima dão `422 amount_below_minimum` ou `422 amount_above_maximum`. - **Único valor mágico do simulador**: saque terminado em `77` na menor unidade (`"2077"`) fica em `processing` até você agir. Qualquer outro valor é pago sozinho. Depósitos não têm valor mágico: o desfecho se controla por `pay`, `expire` e pelo corpo do `pay`. - Para provar `AMOUNT_MISMATCH`, pague com `paid_amount` diferente do valor do depósito. #### Chaves PIX de teste | `key_type` | Valor de teste | Titular que o simulador devolve | |---|---|---| | `cpf` | O CPF do próprio jogador | O próprio CPF (titularidade confirmada) | | `cpf` | Outro CPF de teste | Esse CPF (não bate com o jogador: `document_mismatch`) | | `cnpj` | Um CNPJ válido de teste | O próprio CNPJ (para pessoa física, `document_mismatch`) | | `email`, `phone`, `random` | `chave@operador.test`, `+5511999990000`, qualquer texto | Um CPF sintético, estável para a mesma chave, que não é o do jogador | O nome do titular que o simulador devolve é sempre fictício. ### Exemplos ```json { "external_ref": "jogador-0001", "document": {"type": "CPF", "number": "52998224725", "country": "BR"}, "name": "Jogador Teste", "email": "jogador1@operador.test", "ip": "203.0.113.10" } ``` ### Erros e casos de borda | Você vê | Causa | O que fazer | |---|---|---| | `400 validation_failed`, `reason: reserved_domain_required`, campo `player.email` | E-mail fora de `.test`, `.invalid` e `.example` numa operação Teste | Troque o domínio | | `400 validation_failed`, `reason: format` | E-mail sem `local@dominio.tld` ou com nome de exibição | Envie só o endereço | | `400 validation_failed`, `reason: required_by_operation` | A operação exige um campo que você não mandou | Leia `deposit_requirements` em `GET /v1/currencies` | | `422 document_invalid` | CPF com dígito errado, tamanho errado ou todos iguais | Gere o CPF com o script | | `422 document_mismatch` | Documento diferente do já fixado para esse `external_ref`, ou chave PIX de outro titular | Use um `external_ref` novo, ou a chave do próprio jogador | ### Veja também - [Sandbox](README.md) e [Receitas por caminho](receitas.md). - [Dados e privacidade](../seguranca/dados-e-privacidade.md): como o Dracma trata documento e e-mail. - [Exemplos executáveis](../exemplos/README.md): `cpf-teste.sh` e os testes. --- # Arquivo: docs/seguranca/rede-allowlist-e-mtls.md ## Rede, allowlist e mTLS > Para: Dev · Adm — Conferido em 2026-10-18. Esta página mostra cada camada de proteção entre o seu conector e a API do Dracma: o que está ligado hoje, o que é opcional e o que você vê quando uma camada recusa a chamada. Ela existe para você não esperar uma proteção que não está ativa nem montar um go-live em torno de uma que não existe. ### O que é Uma chamada à API passa por até quatro camadas: | Camada | Estado hoje | O que faz | |---|---|---| | **TLS** | sempre | cifra o tráfego e autentica o servidor | | **Chave e assinatura HMAC** | sempre | prova que quem chama tem o segredo da operação e que o corpo não foi alterado | | **Certificado de cliente (mTLS)** | opcional por instância, **desligado por padrão** | exige certificado apresentado na conexão | | **Lista de IPs de origem** | cadastrável no painel, **não aplicada às chamadas** | restringiria a origem das chamadas | A segurança da chamada hoje se apoia em TLS e na assinatura HMAC com segredo por operação. Veja [Autenticação HMAC](../api/autenticacao.md). ### Quando usar Leia antes de desenhar a rede do seu conector, antes de preencher o formulário de IPs e antes do go-live. O [checklist](../go-live/checklist.md) só cobra o que está ativo. ### Como funciona #### TLS - Todo host do Dracma fala **HTTPS**, com certificado público emitido e renovado automaticamente. A instância de produção só aceita certificado público. - O ingress atende HTTP/1.1 e HTTP/2. Use TLS 1.2 ou superior e valide o certificado do servidor: nunca desligue a verificação no cliente. - O IP de origem de uma chamada vem do **socket** da conexão. Um `X-Forwarded-For` enviado por você não vale. O cabeçalho só é considerado quando o salto anterior é o proxy de borda do próprio Dracma. #### mTLS !!! warning "Não disponível" O Dracma não emite certificados de cliente por operação e não tem autoridade certificadora para isso. O painel mostra "Emissão ainda não disponível" na seção de certificados. O guia **não** exige certificado de cliente no go-live. O mTLS é uma opção de **instância**: se estiver ligado na instância em que você integra, a borda recusa a conexão TLS de quem não apresenta um certificado assinado pela autoridade configurada para aquela instância. A confiança é de **uma autoridade por instância**, não por operação, e a API não cruza a chave com o certificado. Hoje o padrão é desligado. O Dracma avisa no onboarding se a sua instância o exigir. Como o Dracma não emite certificado de cliente, não existe prazo de validade nem rotina de renovação de certificado do lado da API: a regra de "certificado de até 90 dias" não se aplica. Um certificado de cliente recusado na borda não gera `problem+json`: a conexão falha no handshake TLS. #### Lista de IPs de origem !!! warning "Não disponível" O Dracma não restringe hoje as chamadas por IP de origem. A tela de IPs do painel aceita e guarda a lista, mas a lista **não é aplicada**: uma chave e uma assinatura válidas funcionam de qualquer origem. Um IP fora da lista não gera `403` nem alerta. A restrição por IP é um modo de instância que o Dracma pode ligar. Se for ligada na instância em que você integra, valem estas regras: - A borda responde `403` **sem corpo** a qualquer origem fora da lista do operador, antes de a API ver a chamada. - A API responde `403 ip_not_allowed` quando a chave é válida e a origem não está na lista **da operação**. Isso só aparece a quem prova ter o segredo e dispara um alerta no Dracma. - Em produção, uma lista vazia significa **nenhum acesso**. Por isso, mantenha a lista da operação correta mesmo desligada: todos os IPs de saída do conector (cluster, NAT redundante, recuperação de desastre), em IPv4 e IPv6. A lista aceita rede no mínimo `/24` (IPv4) e `/56` (IPv6) e um IP solto vira `/32` ou `/128`. Incluir um IP exige aprovação de outra pessoa e 24 h de espera; remover é imediato. Veja [IPs do conector](../painel/allowlist-de-ip.md). #### Painel e login abertos à internet O painel (`painel.`) e o login (`auth.`) são acessíveis pela internet: **não há trava de IP para pessoas**. A barreira é: 1. login com dois fatores, com sessão de 7 dias (90 com dispositivo confiável); 2. autorização por papel, negando tudo o que não foi concedido; 3. confirmação com fator forte nos últimos 5 minutos para ações sensíveis; 4. aprovação de outra pessoa nas mudanças que pedem 4-eyes; 5. trilha de auditoria encadeada. A API do login nunca é publicada: só as páginas e os endereços OIDC. Veja [Conta, login e dispositivos](../painel/conta-e-login.md). ### Exemplos ```mermaid flowchart LR C["Seu conector
(IP de saída)"] -->|"HTTPS"| B{"Borda do Dracma"} B -->|"caminho fora de /v1/*"| N["404 sem corpo"] B -->|"mTLS ligado e sem certificado"| T["falha no handshake"] B -->|"restrição por IP ligada e IP fora"| F["403 sem corpo"] B -->|"/v1/*"| A{"API"} A -->|"formato, prefixo ou janela inválidos"| U["401 unknown_key,
timestamp_out_of_window"] A -->|"assinatura não confere"| S["401 invalid_signature"] A -->|"chave revogada ou de outro ambiente"| K["401 unknown_key"] A -->|"nonce repetido"| R["401 replayed_nonce"] A -->|"operação suspensa"| P["403 operation_suspended"] A -->|"ok"| O["segue para escopo, limites e a rota"] ``` Hardening do seu lado (opcional, recomendado): - Deixe só o usuário do sistema do conector alcançar o host do Dracma (regra de saída por usuário no firewall). Assim, outro processo comprometido não usa a credencial. - Não filtre a rota do webhook pelo IP de origem: o Dracma não publica a lista de IPs de saída ("Não disponível"). A defesa da rota é a assinatura mais o fetch-back. Veja [Seu endpoint](../webhooks/endpoint-e-politica-de-url.md#ips-de-saida-do-dracma). - Sincronize o relógio por NTP em pelo menos três fontes e alerte quando `|server_time - agora| > 5 s` em `GET /v1/ping`. ### Erros e casos de borda | O que você vê | Camada | Causa provável | |---|---|---| | Falha de TLS, "certificate required" | mTLS | a instância exige certificado de cliente | | `403` sem corpo | borda | restrição por IP ligada e a origem não está na lista | | `403` com `code: ip_not_allowed` | API | chave válida usada fora da lista da operação (só com a restrição ligada) | | `404` sem corpo | borda | caminho fora de `/v1/*` ou `/v1/sandbox/*` num host de operação Real | | `401 unknown_key` | API | chave de outro ambiente, formato inválido ou revogada | | `401 invalid_signature` | API | segredo errado, chave inexistente (mesma resposta), caminho ou corpo diferentes do assinado | | `413` | borda | corpo acima de 1 MB | | `503` com `Retry-After` | API | dependência indisponível; repita com a mesma chave | ### Veja também - [Autenticação HMAC](../api/autenticacao.md) - [Segredos e vazamento](segredos-e-credenciais.md) - [IPs do conector](../painel/allowlist-de-ip.md) - [Ambientes, tipos de operação e hosts](../conceitos/ambientes-e-hosts.md) --- # Arquivo: docs/seguranca/segredos-e-credenciais.md ## Segredos e vazamento > Para: Dev · Adm — Conferido em 2026-10-18. Esta página diz onde guardar cada segredo, como rotacionar sem derrubar a integração e o que fazer, em ordem, quando um segredo vaza. Leia o procedimento de emergência **antes** do go-live: no meio de um incidente, as esperas de 24 h do painel não deixam improvisar. ### O que é Três materiais sensíveis da sua integração: | Material | Formato | Quem vê | Validade | |---|---|---|---| | Chave de API | key id `key_test_…` ou `key_live_…` e um segredo | o segredo aparece **uma vez**, a quem pediu | sem prazo; vale até ser revogada | | Segredo de webhook | `whsec_…` | aparece **uma vez**, a quem gira o segredo | sem prazo; rotação com sobreposição de 7 dias | | Credenciais de pessoas | senha, passkey, app autenticador, códigos de recuperação | cada pessoa | veja [Conta, login e dispositivos](../painel/conta-e-login.md#fatores-codigos-de-recuperacao-e-e-mails) | Duas observações: - O Dracma guarda o segredo da chave cifrado e o devolve apenas na revelação. **Ele não consegue mostrar o segredo de novo.** Segredo perdido é segredo substituído. - O prazo de 90 dias para trocar a chave é uma **recomendação**. O Dracma não expira chaves ativas nem avisa aos 75 dias. ### Quando usar Use esta página ao montar o deploy do conector, ao agendar a rotação periódica e quando houver suspeita de vazamento (commit com segredo, log com cabeçalho, ex-funcionário com acesso, máquina comprometida). ### Como funciona #### Onde guardar 1. **Cofre:** gerenciador de segredos da sua empresa. É a única cópia permanente. 2. **No host:** arquivo legível **só** pelo usuário do sistema que roda o conector (permissão `0400`), gravado pela ferramenta de deploy a partir do cofre. Nunca à mão no servidor. 3. **Nunca:** - em repositório de código, imagem de contêiner ou artefato de build; - em log, ticket, chat ou e-mail; - em variável de front (`NEXT_PUBLIC_*`, `VITE_*`) ou em app de jogador: o Dracma é chamado **só do servidor**; - na linha de comando, onde `ps` mostra o valor; - em cabeçalho `Dracma-Signature` registrado por inteiro. O segredo do webhook fica em um arquivo com **uma linha por segredo ativo** (um, ou dois durante a rotação), para o verificador aceitar os dois. #### Rotação planejada sem downtime Chave de API: 1. Uma pessoa pede uma chave nova no painel, com fator forte. **Outra pessoa** aprova. A chave só nasce depois de **24 horas** de espera. 2. Quem pediu revela o segredo em até 24 h, uma vez, com fator forte. 3. Entregue o segredo ao conector pelo cofre e faça o deploy. Uma operação aceita até **duas chaves ativas**, então a antiga continua valendo. 4. Confira no painel o "último uso" de cada chave. Quando a antiga estiver sem uso e sem erros, revogue-a. 5. A revogação é imediata, de uma pessoa, com motivo e fator forte, e vale em todas as réplicas em até 30 segundos. Segredo de webhook: 1. Gire o segredo no painel (uma pessoa com permissão de gerir webhooks, com fator forte). O valor novo aparece uma vez. 2. Durante **7 dias** o Dracma assina com os dois segredos (dois `v1=` no cabeçalho). Acrescente o novo ao arquivo do receptor e confirme que as entregas continuam aceitas. 3. Depois dos 7 dias, remova o antigo do arquivo. Veja [Chaves de API](../painel/chaves-de-api.md#rotacionar-sem-parar-a-integracao) e [Segredo e rotação](../webhooks/segredos-e-rotacao.md#rotacao). #### Procedimento de emergência: um segredo vazou Defina antes do go-live **quem** faz cada passo. **Chave de API vazada** 1. **Peça já uma chave de reposição** (aprovação de outra pessoa e 24 h de espera correm em paralelo). Se já houver duas chaves ativas, revogue a que não é mais usada primeiro. 2. **Revogue a chave vazada.** É imediato (uma pessoa, motivo, fator forte) e efetivo em até 30 segundos. Depois disso a chave recebe `401 unknown_key`. 3. Se a revogação deixar a integração sem chave até a reposição ficar pronta, **aceite o tempo parado**: o seu conector continua subindo sem o Dracma (pagamentos ficam indisponíveis ou em fila). A alternativa, manter a chave vazada ativa, é pior. 4. Revele a chave de reposição, entregue pelo cofre e faça o deploy. 5. Procure no painel (último uso, contadores de recusa e trilha de auditoria) chamadas que você não reconheça. 6. Se houver sinal de uso indevido, **suspenda o saque automático** na tela "Regras, listas e emergência": é imediato, zera o automático da moeda e uma segunda pessoa confirma em até 24 h. Os saques passam a ir para revisão manual. Para religar, abra uma proposta de mudança de regra. !!! warning "Não disponível" O painel não tem botão "Suspender operação". Se precisar interromper **todas** as chamadas de uma operação (elas passam a responder `403 operation_suspended`), peça ao suporte do Dracma pelo canal do onboarding. **Segredo de webhook vazado** 1. Gire o segredo (uma pessoa, fator forte). O Dracma passa a assinar com o novo e com o antigo por 7 dias. 2. Atualize o receptor para aceitar o novo. 3. Enquanto o antigo valer, o **fetch-back** protege você: um evento forjado com assinatura válida não credita nada, porque o valor vem de `GET /deposits/{id}`. !!! warning "Não disponível" O painel não encerra a sobreposição de 7 dias antes do prazo. Para encerrá-la antes, peça ao suporte do Dracma. **Credencial de pessoa vazada** 1. A pessoa (ou outro administrador) encerra todas as sessões em "Conta, Sair de todos os dispositivos". 2. Troque a senha e os fatores. Remover o último fator forte tem atraso de 24 h cancelável. 3. Um administrador remove os pacotes da pessoa em "Usuários e papéis", se for o caso. ### Exemplos Verificar a permissão do arquivo do segredo no host: ```bash stat -c '%a %U' /caminho/seguro/dracma-live.key # esperado: 400 grep -r "key_live_" ./repositorio-do-conector # esperado: sem resultado ``` ### Erros e casos de borda | Situação | O que acontece | O que fazer | |---|---|---| | Perdi o segredo da chave | não há como mostrar de novo | peça uma chave nova | | A janela de 24 h para revelar venceu | a chave nunca foi revelada | peça outra | | Já existem duas chaves ativas | `key_limit_reached` | revogue uma antes de pedir | | O segredo foi colado num ticket ou chat | está vazado | trate como vazamento | | Só uma pessoa na equipe | os pedidos de chave não fecham | tenha ao menos duas pessoas (veja [O que preparar](../onboarding/pre-requisitos.md)) | ### Veja também - [Rede, allowlist e mTLS](rede-allowlist-e-mtls.md) - [Chaves de API](../painel/chaves-de-api.md) - [Segredo e rotação](../webhooks/segredos-e-rotacao.md) - [Checklist de go-live](../go-live/checklist.md) --- # Arquivo: docs/seguranca/dados-e-privacidade.md ## Dados pessoais > Para: Dev · Fin — Conferido em 2026-10-18. Quais dados pessoais passam pelo Dracma, como são protegidos, o que o painel mascara, o que você precisa cuidar do seu lado e como lidar com a troca de documento de um jogador. ### O que é #### Dados que você envia | Dado | Campo | Observação | |---|---|---| | Identificador do jogador | `player.external_ref` | opaco e imutável. **Nunca** e-mail, telefone ou CPF | | Documento do jogador | `player.document` | CPF ou CNPJ para PIX. Só dígitos | | Nome, e-mail, IP, dispositivo | `player.name`, `player.email`, `player.ip`, `player.device_id` | a operação diz o que exige em `deposit_requirements` | | Chave PIX de destino | `destination.pix.key` | dado pessoal (CPF, e-mail, telefone ou chave aleatória) | | Metadados livres | `metadata` | **não coloque dado pessoal aí** | Mande só o que a operação exige e o que as regras usam: esse é o princípio da minimização. #### Dados que o Dracma devolve - O depósito pago ou em revisão traz `payer`: o `document` completo e o `name` de **quem pagou o PIX**, mais `matches_player`. É dado pessoal em claro na resposta e no corpo do webhook. - O depósito e o saque devolvem o `player_ref`, nunca o documento do jogador que você enviou. - O painel mascara documento e chave PIX por padrão (veja abaixo). #### Como o Dracma protege - O documento do jogador e os dados do pagador são **cifrados em repouso**, com chaves separadas por tenant e por operação. Um dado de uma operação não abre em outra. - A comparação de documentos (para as regras e para o `document_mismatch`) usa uma impressão digital irreversível, não o documento em claro. - A identidade de **pessoas da sua equipe** é um identificador interno (UUID). Listas e trilhas mostram só o e-mail mascarado (`an***@dominio.example`). - Os testes e a homologação usam só dados sintéticos e domínios reservados. ### Quando usar Consulte antes de desenhar logs, filas e telas de suporte do seu conector, e quando a equipe de compliance perguntar onde ficam os dados pessoais. ### Como funciona #### O documento fica fixado O primeiro documento enviado para um `player.external_ref` fica **fixado** naquele jogador. Enviar outro documento depois, em depósito ou saque, é recusado com `422 document_mismatch`. Isso impede que um `external_ref` seja reaproveitado por outra pessoa e que o limite de um jogador seja contornado. Para saque existe um caso parecido: a chave PIX de destino precisa pertencer ao mesmo titular; quando o Dracma descobre o contrário depois, o saque é rejeitado. !!! warning "Não disponível" A API não troca o documento de um `external_ref` já usado. Se o cadastro tinha um documento errado, abra um chamado no canal de suporte com o `external_ref` e o motivo. Não crie outro `external_ref` para a mesma pessoa só para contornar a recusa: o Dracma passa a tratá-la como jogador novo para as regras (por exemplo, o primeiro saque volta a ir para revisão). #### Painel: máscara por padrão - Documento e chave PIX aparecem mascarados nas filas, nas transações e nos relatórios. - **Revelar** um dado exige motivo, confirmação recente com fator forte e fica na trilha de auditoria. Podem revelar os pacotes Financeiro, Revisor e Auditor (veja [Equipe e papéis](../painel/equipe-e-papeis.md#o-que-cada-pacote-pode-fazer)). - Exportar relatório **com** dados pessoais exige a aprovação de outra pessoa e o download vale por tempo limitado. Veja [Relatórios](../painel/relatorios.md#exportar-com-dados-pessoais). #### O que fazer do seu lado 1. **Logs:** não registre segredo, `Dracma-Signature` inteiro, documento completo, chave PIX em claro nem o corpo cru de um webhook sem mascarar. Registre o `Dracma-Trace-Id`, o `external_id` e o `id` do Dracma. 2. **Armazenamento:** guarde o documento do jogador com cifra e controle de acesso no seu sistema. O Dracma já o tem; você só precisa dele para o seu cadastro. 3. **Webhooks:** o corpo do evento tem a mesma forma do `GET`, inclusive `payer`. Trate o receptor como sistema que manipula dado pessoal. 4. **Identificadores:** `external_id` e `external_ref` aceitam só `A-Z a-z 0-9 _ . : -`. Use UUIDs, não CPF nem telefone. 5. **Ambientes:** nunca copie dados reais de jogadores para o hml ou para fixtures. No hml, e-mails fora dos domínios de teste recebem mensagem de verdade (login, convites e alertas). Veja [Dados de teste](../sandbox/dados-de-teste.md#e-mail-real-no-hml-painel-e-convites). ### Exemplos Trecho do `payer` de um depósito pago (objeto `Deposito`, dados fictícios): ```json { "document": { "type": "CPF", "number": "52998224725", "country": "BR" }, "name": "Jogador Teste", "matches_player": true } ``` Esse trecho é dado pessoal. Em log, registre `matches_player` e o `id` do depósito, não o objeto inteiro. ### Erros e casos de borda | Situação | O que acontece | O que fazer | |---|---|---| | Documento diferente para o mesmo `external_ref` | `422 document_mismatch`, sem criar objeto | corrija o cadastro; se o documento antigo estava errado, abra chamado | | Pagador diferente do jogador | depósito vai para revisão com `PAYER_MISMATCH` | não credite; veja [Revisão](../painel/revisao-de-depositos-e-saques.md#decidir-um-item) | | Chave PIX de outro titular | saque recusado ou rejeitado depois | mostre mensagem genérica ao jogador | | Pedido de exclusão ou anonimização de um jogador | sem rota na API | peça pelo canal de suporte | !!! warning "Não disponível" A API não oferece rota para apagar ou anonimizar os dados de um jogador, e este guia não afirma prazo de retenção. As responsabilidades de cada lado sobre dados pessoais (controlador e operador, base legal, retenção) ficam no contrato com o Dracma. ### Veja também - [Convenções HTTP](../conceitos/convencoes-http.md) - [Segredos e vazamento](segredos-e-credenciais.md) - [Relatórios](../painel/relatorios.md) - [Dados de teste](../sandbox/dados-de-teste.md) --- # Arquivo: docs/go-live/checklist.md ## Checklist de go-live > Para: Dev · Fin · Adm — Conferido em 2026-10-18. Você só vai ao ar com a operação Real depois de cumprir esta lista na operação **Teste**. Cada item tem uma **prova**: saída de teste, print do painel, log ou dashboard, e não uma promessa. Item sem prova conta como não feito. ### O que é Um checklist único para as três frentes: o conector (Dev), o dinheiro (Fin) e o painel (Adm). Ele junta o que antes estava separado em lista geral, lista por operador e itens de painel. Copie a lista para o seu tracker, preencha a prova de cada item e anexe o resultado ao registro de go-live descrito em [Dia do corte e rollback](dia-do-corte-e-rollback.md). ### Quando usar Antes do pedido de operação Real (passo 10 da [linha do tempo](../onboarding/README.md)): as seções 1 a 9 se provam na operação Teste. A seção 10 se prova no painel e a seção 11 no dia do corte. ### Como funciona #### 1. Conformidade - [ ] **Suíte de conformidade verde** contra a operação Teste, rodada com a URL de webhook real do seu conector ([Suíte de conformidade](../sandbox/conformidade.md)). *Prova:* saída com todos os casos aprovados e a data. - [ ] Seu cliente reproduz os **vetores de assinatura** byte a byte (as três chamadas, os dois webhooks válidos e a recusa do negativo) no seu CI. *Prova:* teste no seu repositório, como os de [Exemplos](../exemplos/README.md). - [ ] `GET /v1/ping` com a chave de Teste devolve `environment: "sandbox"` e o `operation_id` esperado. *Prova:* saída do comando. #### 2. Chamadas - [ ] Nonce por **CSPRNG**, 32 caracteres hexadecimais minúsculos, novo a cada requisição, inclusive em nova tentativa. - [ ] Timestamp em **segundos**. Host com **NTP** e alerta de desvio acima de 5 s contra o `server_time` de `GET /v1/ping`. *Prova:* regra de alerta versionada. - [ ] Timeout de cliente de **10 s**. - [ ] Todo `POST` leva `Idempotency-Key`. Em depósito e saque, `Idempotency-Key` = `external_id`, gravado no seu banco **antes** da chamada (outbox). *Prova:* teste que mata o processo entre a gravação e a chamada; o worker retoma com a mesma chave e cria um único objeto. - [ ] Regra de ouro: `4xx` não repete; `5xx`, `429` e erro de rede repetem com a mesma chave; `code` desconhecido é tratado pela classe HTTP ([Idempotência e repetição](../api/idempotencia.md#arvore-de-decisao)). *Prova:* caso de teste com `POST /sandbox/outage` `{"down": true}`: `503`, depois retomada com a mesma chave **sem duplicar**. - [ ] Valor convertido para a menor unidade **sem float**. *Prova:* teste de unidade com `50`, `50.5`, `0.01`, `50.001` (recusa) e `1e3` (recusa). - [ ] `Dracma-Trace-Id` registrado em todo log de chamada ao Dracma. #### 3. Webhooks - [ ] Verificação da assinatura sobre o **corpo cru**, janela de 300 s, `Dracma-Timestamp` igual ao `t=`, aceita **qualquer** `v1` com **qualquer** segredo ativo (lista para a rotação), comparação em tempo constante. - [ ] `Dracma-Event-Id` igual ao `id` do corpo; senão `400`. - [ ] **Deduplicação** por `Dracma-Event-Id` com índice único e retenção de pelo menos 7 dias; duplicata devolve `200` sem efeito. *Prova:* `POST /sandbox/webhooks/redeliver` de um evento já processado: `200`, saldo inalterado. - [ ] **Fetch-back obrigatório** antes de mover dinheiro: o crédito sai do `GET /deposits/{id}`, nunca do corpo do webhook. *Prova:* um `deposit.paid` **forjado**, com assinatura válida, para um depósito `pending` na API **não credita**. - [ ] No fetch-back, confere `external_id` (é seu), `amount`, `currency` e `player_ref` com o que gravou. Divergência: não credita, marca para revisão e alerta. - [ ] Crédito e débito idempotentes por `external_id`: um depósito credita **uma** vez, mesmo com dois eventos para o mesmo objeto. - [ ] Ordem não garantida: `version` menor ou igual ao já aplicado é ignorada, com `200`. - [ ] **Leitor tolerante:** tipo de evento, `status`, `method` ou campo desconhecido devolve `200`, grava, **não move dinheiro** e é conferido depois. - [ ] Responde em até **10 s**; trabalho pesado vai para fila local. - [ ] A rota do webhook não usa autenticação de usuário. O Dracma não publica lista de IPs de saída, então não filtre essa rota por IP de origem; a defesa é a assinatura mais o fetch-back ([Seu endpoint](../webhooks/endpoint-e-politica-de-url.md#ips-de-saida-do-dracma)). #### 4. Depósitos - [ ] **Só `paid` credita**, pelo valor **registrado na criação** ([Ciclo do depósito](../dinheiro/ciclo-do-deposito.md)). - [ ] `under_review`, `pending`, `created` e qualquer estado desconhecido não creditam. *Prova:* receitas de `PAYER_MISMATCH` e de valor divergente no Teste ([Receitas](../sandbox/receitas.md)). - [ ] `refunded` e `chargeback` depois de `paid`: política definida e escrita. Recomendação: não mover automaticamente; abra uma exceção (`needs_decision`) com alerta e deixe uma pessoa decidir ([Política de exceções](../conector/politica-de-excecoes.md)). - [ ] Pagamento depois de expirar ou cancelar (`paid_amount` em objeto terminal): não credita; vai para exceção. - [ ] `player.external_ref` opaco e imutável; documento consistente entre depósito e saque. - [ ] **Exigências por operação** lidas de `GET /v1/currencies` (`method_details[].deposit_requirements`) no boot e periodicamente. *Prova:* depósito sem o campo exigido volta `400` com `reason: required_by_operation`, tratado como recusa sem repetição. - [ ] Fora de produção, e-mails de jogador só de domínio reservado (`.test`, `.invalid`, `.example`). - [ ] Mostra ao jogador o `instructions.pix.expires_at` **devolvido**. #### 5. Saques - [ ] **Reserva** do saldo do jogador **antes** do `POST /withdrawals`, na mesma transação que grava o `external_id`. - [ ] A reserva só é devolvida em `rejected`, `failed` ou `cancelled`, confirmados por fetch-back, ou em `4xx` conclusivo na criação. **Nunca** em timeout ou `5xx` sem resposta conclusiva. - [ ] `paid` finaliza a reserva e guarda `psp_reference`. - [ ] `player.ip` com o IP **do jogador** em todo saque (sem ele o saque nunca é automático). *Prova:* no Teste, um saque dentro do limite da regra, com `player.ip`, vira `approved`. - [ ] Chave PIX normalizada (tipos `cpf`, `cnpj`, `email`, `phone`, `random`; telefone em `+55…`). *Prova:* teste de unidade. - [ ] `returned` depois de `paid`: política definida; recomendação de não recreditar automaticamente. - [ ] `409 idempotency_key_reused` no envio vira exceção, com a reserva mantida. - [ ] `review_requested: true` quando a sua pré-checagem desconfiar (só torna a decisão **mais** restritiva). - [ ] `decision.reasons` aparece para o seu time de suporte e risco; **nenhum** motivo de bloqueio vai ao jogador (mensagem genérica). - [ ] *Prova:* no Teste, um saque acima do limite vai para `under_review` com `AUTO_MULTIPLIER_EXCEEDED`, e `reject` e `fail` devolvem a reserva exatamente uma vez. #### 6. Dinheiro: provas - [ ] **Uma reserva por saque.** Repetir o `POST` com a mesma chave não cria segunda reserva nem segundo saque. *Prova:* o teste da seção 2 e uma consulta ao seu livro com uma linha de reserva por `external_id`. - [ ] **Uma devolução por saque.** Receitas `reject` e `fail` no Teste: a reserva volta uma vez; repetir o evento não devolve de novo. *Prova:* livro do jogador antes e depois. - [ ] **Um crédito por depósito.** Receita de `redeliver`: continua um crédito. - [ ] **O seu livro bate com o do Dracma.** Um dia de Teste reconciliado nos dois lados, sem divergência ([Reconciliar com o seu livro](../dinheiro/reconciliacao-do-operador.md)). *Prova:* relatório do dia, digest e assinatura conferidos, e o seu extrato por `external_id`. - [ ] **Limites conservadores** configurados na operação Real. Leia a regra ativa em `GET /v1/rules` ou na tela "Regras e listas"; proponha a mudança pelo painel ou por `POST /v1/rules/change-requests` (escopo `rules:propose`). Endurecer vale na hora; afrouxar pede aprovação de outra pessoa e 24 h. Este guia não publica valores padrão. *Prova:* print da regra ativa e do pedido aprovado. - [ ] **Caixa inicial conferido** em `GET /v1/balances`. O caixa de saques só cresce com depósitos pagos e devoluções. *Prova:* leitura de `GET /v1/balances` depois do primeiro depósito pago. !!! warning "Não disponível" O Dracma não oferece aporte de caixa para saques. Sem depósito pago, o caixa livre é zero e o saque é recusado com `422 insufficient_operation_balance`. Planeje o primeiro saque real depois do primeiro depósito real. #### 7. Recuperação e reconciliação - [ ] Job de `GET /events?after=<último evento>` a cada 5 minutos, com cursor persistido, passando pelo mesmo dedup e fetch-back do webhook. *Prova:* teste com o receptor desligado durante um pagamento; o job credita uma vez em até 5 minutos ([`GET /events`](../webhooks/recuperacao-get-events.md)). - [ ] Reconciliação diária por moeda (`GET /reconciliation`) com **digest e assinatura conferidos**. - [ ] **Alerta de divergência:** todo `paid` do Dracma tem crédito ou débito seu, e vice-versa. Qualquer diferença abre incidente. *Prova:* um dia de Teste comparado, zero divergência. - [ ] Alertas para: webhook com assinatura inválida, `401` ou `403` na sua saída, fila de exceções parada acima de 4 h, job de `/events` atrasado, desvio de relógio. - [ ] A fila de exceções do seu produto tem permissão própria, confirmação forte e motivo. *Prova:* print e teste de permissão. #### 8. Segredos e rede - [ ] Chave, segredo de webhook e demais segredos no **cofre**, entregues ao host pela ferramenta de deploy, arquivo `0400` do usuário do conector. *Prova:* `stat` do arquivo e busca por `key_live_` no repositório e na imagem, sem resultado ([Segredos e vazamento](../seguranca/segredos-e-credenciais.md)). - [ ] O Dracma é chamado **só do servidor**; nenhum app ou navegador de jogador fala com ele. - [ ] Logs **não** registram segredo, `Dracma-Signature` inteiro, documento completo nem chave PIX em claro ([Dados pessoais](../seguranca/dados-e-privacidade.md)). - [ ] Opcional: saída para o host do Dracma restrita ao usuário do conector. A lista de IPs de origem do painel está cadastrada, embora hoje não seja aplicada ([Rede, allowlist e mTLS](../seguranca/rede-allowlist-e-mtls.md)). - [ ] Rotação de chave e de segredo de webhook **ensaiada** no Teste sem downtime, com o verificador aceitando dois segredos ao mesmo tempo. - [ ] Procedimento de **emergência escrito**: quem revoga a chave, quem pede a reposição, como a chave nova chega ao conector e quanto tempo isso leva (24 h de espera + aprovação). #### 9. Ambientes - [ ] Homologação aponta para o host de Teste com `key_test_`; produção aponta para o host de Real com `key_live_`. *Prova:* chamar o host de Real com a chave de Teste devolve `401 unknown_key`. - [ ] Fixtures, seeds e testes usam só domínios reservados e documentos de teste; nenhum dado real de jogador fora de produção. - [ ] Os testes automáticos não chamam `/v1/sandbox/*` contra a operação Real (essas rotas respondem `404`). #### 10. Painel e equipe - [ ] **Pelo menos duas pessoas** com acesso, cada uma com dois fatores e um fator forte. *Prova:* lista "Quem tem acesso" ([Equipe e papéis](../painel/equipe-e-papeis.md)). - [ ] **Operação Real aprovada** e chave `key_live_` revelada (janela de 24 h). *Prova:* `GET /v1/ping` com a chave nova devolve `environment: "production"` ([De Teste para Real](../onboarding/promocao-para-producao.md)). - [ ] **Conexão de gateway ativa** na operação Real. *Prova:* print da tela "Gateways" ([Gateways](../painel/gateways-e-psp.md)). - [ ] **Webhook de produção** cadastrado em `https` e **primeiro segredo** girado e instalado no receptor. *Prova:* evento entregue ao receptor de produção. - [ ] **Alertas configurados**, no mínimo caixa baixo e webhook falhando ([Alertas e e-mails](../painel/alertas.md#configurar-uma-regra)). *Prova:* print da tela e um disparo de teste. - [ ] Contatos de alerta de e-mail informados ao Dracma, porque o painel não os edita. - [ ] Suspensão do saque automático ensaiada em operação Teste, com a segunda pessoa confirmando. *Prova:* registro na lista de emergências. ### Exemplos Linha de tracker preenchida: | Item | Responsável | Prova | Data | |---|---|---|---| | Dedup por `Dracma-Event-Id` | Dev | saída do `redeliver`: `200` sem crédito novo | 2026-10-12 | | Limites conservadores | Fin | print da regra ativa e do pedido aprovado | 2026-10-14 | | Gateway ativo | Adm | print de "Gateways" | 2026-10-15 | ### Erros e casos de borda - **Item que não se aplica:** escreva por que não se aplica. Em branco conta como não feito. - **Prova antiga:** refaça a prova se você mudou o conector, a regra ou a chave depois dela. - **Seção 10 sem pessoas suficientes:** com uma só pessoa, os pedidos do painel não fecham. Convide a segunda antes de pedir a operação Real. ### Veja também - [Dia do corte e rollback](dia-do-corte-e-rollback.md) - [De Teste para Real](../onboarding/promocao-para-producao.md) - [Suíte de conformidade](../sandbox/conformidade.md) - [Referência rápida](../referencia-rapida.md) --- # Arquivo: docs/go-live/dia-do-corte-e-rollback.md ## Dia do corte e rollback > Para: Adm · Dev — Conferido em 2026-10-18. O dia do corte é o dia em que o seu produto passa a usar a operação Real. Esta página dá a ordem das ações, os critérios para seguir, a primeira transação real acompanhada e o plano de volta. Faça-a depois de cumprir o [checklist de go-live](checklist.md). ### O que é Um roteiro de execução com responsáveis e horários, mais o plano de rollback. Rollback aqui significa **tirar o seu produto da operação Real sem perder dinheiro nem duplicar nada**. O Dracma não desfaz pagamentos já feitos: o que muda é o seu lado parar de criar novos depósitos e saques e terminar os que estão em andamento. O seu produto sobe e funciona sem o Dracma. Desligar o método PIX do seu lado não derruba o resto do sistema. ### Quando usar No dia marcado do corte, e novamente se for preciso voltar atrás. Escolha uma janela de baixo movimento, com as duas pessoas que aprovam pedidos no painel disponíveis. ### Como funciona #### Antes do dia | Quando | O que | Quem | |---|---|---| | 5 dias antes | pedido de operação Real aprovado, chave revelada e instalada no ambiente de produção do conector, **ainda desligada para jogadores** | Adm e Dev | | 3 dias antes | gateway ativo, webhook de produção com o primeiro segredo, limites conservadores, alertas | Adm e Fin | | 2 dias antes | `GET /v1/ping` com a chave nova mostra `environment: "production"`; ensaio do plano de volta | Dev | | 1 dia antes | congelamento de mudanças no conector; checklist 100% com prova; as duas pessoas de aprovação confirmadas | todos | Qualquer mudança de chave, IP ou webhook feita pelo painel leva **24 horas** de espera. Não deixe nada disso para o dia do corte. #### Ordem do dia 1. **Congele** deploys no conector. 2. **Confirme** que o checklist está completo e que as duas pessoas de aprovação estão online. 3. **Ping:** `GET /v1/ping` com a chave `key_live_` devolve `environment: "production"` e o `operation_id` da operação Real. 4. **Receptor:** confirme que o primeiro segredo de webhook foi girado e está instalado no receptor de produção. Em operação Real não há teste de URL: o primeiro evento chega com o depósito do passo seguinte. 5. **Primeira transação real de valor mínimo, depósito:** crie pela tela "Cobrança de teste" (confirme "é REAL") ou pela API, pague o PIX e acompanhe do `POST` ao `deposit.paid`, ao fetch-back, ao crédito e à linha da reconciliação. 6. **Primeira transação real de valor mínimo, saque:** só depois do depósito pago, porque o caixa vem dos depósitos. Acompanhe a reserva, a decisão, o `withdrawal.paid` e a finalização da reserva. 7. **Caixa:** confira `GET /v1/balances` e o limiar de alerta de caixa baixo. 8. **Liberação gradual:** ligue o método para uma fração dos jogadores, por exemplo 1%, depois 10%, depois 100%, com intervalos para olhar os painéis. 9. **Registro de go-live:** preencha e assine. 10. **No dia seguinte:** depois das 02:00 UTC, reconcilie o dia nos dois lados. ```mermaid flowchart TD A["Congelar e confirmar o checklist"] --> B["Ping de produção ok?"] B -- "não" --> X["Parar: corrigir chave ou host"] B -- "sim" --> C["Depósito real de valor mínimo"] C --> D{"Crédito e reconciliação batem?"} D -- "não" --> R["Rollback"] D -- "sim" --> E["Saque real de valor mínimo"] E --> F{"Reserva, pagamento e livro batem?"} F -- "não" --> R F -- "sim" --> G["Liberar 1%, 10%, 100%"] G --> H{"Alertas ou divergências?"} H -- "sim" --> R H -- "não" --> I["Registro de go-live e reconciliação do dia seguinte"] ``` #### Critérios para voltar atrás Volte se qualquer um acontecer: - crédito duplicado ou crédito sem `paid` confirmado por fetch-back; - reserva de saque que não volta, ou que volta duas vezes; - divergência na reconciliação sem explicação em uma hora; - webhooks falhando de forma contínua, sem o job de `GET /events` compensar; - `401` ou `403` inesperados em produção; - suspeita de vazamento de segredo (siga o procedimento de [Segredos e vazamento](../seguranca/segredos-e-credenciais.md#procedimento-de-emergencia-um-segredo-vazou)). #### Rollback **No seu produto (a parte que importa):** 1. **Desligue o método PIX para novos pedidos** com a chave de recurso que você preparou. O resto do sistema segue de pé. 2. **Mantenha o receptor de webhook e o job de `GET /events` ligados.** Depósitos já criados ainda podem ser pagos até expirar, e saques em andamento ainda terminam. Cada evento continua passando por deduplicação e fetch-back. 3. **Não revogue a chave** enquanto houver depósito pendente ou saque em andamento: você perderia o fetch-back. 4. **Devolva a reserva** só nos estados terminais de falha confirmados por fetch-back, nunca por timeout. 5. **Reconcilie** o dia e trate as exceções (pagamento após expirar, estorno) pela política de exceções. **No Dracma:** - **Suspender o saque automático** da moeda, na tela "Regras, listas e emergência": é imediato, zera o automático e o multiplicador, e outra pessoa confirma em até 24 h. Os saques novos vão para revisão manual. Para religar, use uma proposta de mudança de regra. - **Revogar a chave** (uma pessoa, motivo, fator forte), efetivo em até 30 segundos, quando for seguro. - **Interromper todas as chamadas de uma operação:** peça ao suporte do Dracma. As chamadas passam a responder `403 operation_suspended`. !!! warning "Não disponível" O painel não tem botão para suspender uma operação nem para apagar uma operação ou um tenant. Pare pelo seu lado e use a emergência do saque automático. Para suspender ou apagar, peça ao suporte do Dracma. **Voltar para o Teste:** aponte o conector de homologação de novo para o host de Teste com `key_test_`. A operação Real continua existindo, com a mesma chave e o mesmo saldo; ela não é apagada. ### Exemplos Registro de go-live (copie e preencha): | Campo | Valor | |---|---| | Operação Real e `operation_id` | | | Data e hora do corte (UTC e fuso da operação) | | | Versão do conector e da suíte de conformidade | | | `external_id` e `Dracma-Trace-Id` do depósito mínimo | | | `external_id` e `Dracma-Trace-Id` do saque mínimo | | | Linha da reconciliação do dia | | | Caixa inicial lido em `GET /v1/balances` | | | Plano de volta ensaiado em (data) | | | Responsável do seu lado (nome, assinatura) | | | Responsável do Dracma (nome, assinatura) | | ### Erros e casos de borda | Situação | O que fazer | |---|---| | O saque mínimo é recusado por `insufficient_operation_balance` | o caixa ainda não tem depósito pago; espere o depósito do passo 5 | | `401 unknown_key` no corte | o host e a chave não combinam: Real usa `key_live_` no host de Real | | `404` em `/v1/sandbox/...` | seus testes de simulação estão apontando para Real; rode-os só no Teste | | O PIX do passo 5 não chega ao `paid` | confirme na tela "Gateways" que a conexão está ativa e olhe o status do depósito | | Pedido de chave nova no meio do corte | leva 24 h de espera; use a chave de reposição preparada antes | | Dracma indisponível durante o corte | o conector segue de pé; pagamentos ficam em fila ou indisponíveis; retome com a mesma `Idempotency-Key` | ### Veja também - [Checklist de go-live](checklist.md) - [De Teste para Real](../onboarding/promocao-para-producao.md) - [Segredos e vazamento](../seguranca/segredos-e-credenciais.md) - [Runbook de dinheiro](../dinheiro/runbook.md) --- # Arquivo: docs/versionamento/politica.md ## Versionamento > Para: Dev — Conferido em 2026-10-18. A API e os eventos do Dracma evoluem só por **acréscimo** dentro da v1. Uma mudança incompatível só existe como `/v2`. Esta página define o que é compatível, o que o seu código precisa tolerar e onde acompanhar as mudanças. ### O que é A versão maior vai **no caminho**: toda rota pública começa com `/v1`. Não há cabeçalho de versão nem negociação por `Accept`. O contrato (OpenAPI) tem uma versão própria, hoje **1.1.0**, que sobe a cada acréscimo. | Item | Regra | |---|---| | `/v1` | Só recebe acréscimos. Nada que funciona hoje muda de significado. | | `/v2` | Reservada para mudança incompatível. Não existe hoje. | | Versão do contrato (`1.x.y`) | `x` sobe com funcionalidade nova e aditiva; `y`, com esclarecimento ou campo aditivo pequeno. | | Eventos de webhook | Mesma regra da API. O `type` público do evento não leva sufixo de versão. | !!! warning "Não disponível" O Dracma não envia cabeçalhos `Deprecation` ou `Sunset` nem um cabeçalho de versão nas respostas. Você acompanha as mudanças pelo [Changelog](changelog.md) e pelo documento OpenAPI. ### Quando usar Consulte esta página para decidir se uma mudança que você viu no Dracma é esperada, ao escrever o leitor de respostas e de webhooks, e ao planejar a atualização do conector quando sair uma versão nova do contrato. ### Como funciona #### O que é compatível (acontece dentro da v1) Qualquer um destes itens pode aparecer a qualquer momento, sem aviso prévio e sem mudar o número `1`: - **campo novo** em qualquer objeto de resposta, de evento ou de webhook; - **valor novo** em enumeração aberta: `status` de depósito e saque, `method`, `rail`, `kind` de item de reconciliação, `type` de evento, `reason` de validação, `code` de erro, `reason_code` de devolução; - **rota nova** ou **parâmetro opcional novo** em rota existente; - **evento novo** no catálogo; - campo que hoje vem `null` ou vazio passar a vir preenchido; - erro de validação mais claro (mais campos em `errors[]`). #### O que é incompatível (só em `/v2`) - remover ou renomear campo, rota, evento ou valor existente; - mudar o tipo de um campo (de `string` para número, por exemplo); - mudar o significado de um estado, de um evento ou de um `code`; - tornar obrigatório um campo que era opcional, ou apertar a validação de um campo que já era aceito; - mudar o algoritmo de assinatura ou o formato do pedido canônico. ```mermaid flowchart TD A[Mudança proposta] --> B{Quebra algum conector que lê a v1 corretamente?} B -->|não: só acrescenta| C[Fica na v1 e entra no Changelog] B -->|sim| D[Só como /v2, lado a lado com a v1] ``` #### O seu lado: leitor tolerante A promessa de "só acréscimo" vale se o seu código não quebra com acréscimo. Faça assim: 1. **Ignore campo desconhecido** em qualquer JSON de resposta e de webhook. Não use desserialização estrita que falha com campo extra. 2. **Trate valor desconhecido como não terminal.** Um `status` ou `kind` que você não conhece não move dinheiro no seu livro: grave o objeto e confira depois pelo fetch-back (`GET /deposits/{id}` ou `GET /withdrawals/{id}`). 3. **Evento de `type` desconhecido:** responda `200` ao webhook, guarde e não mova dinheiro ([Receptor de webhooks](../webhooks/receptor.md)). 4. **`code` desconhecido:** decida pela classe HTTP e por `retryable` ([Erros](../api/erros.md#o-que-fazer-com-cada-resposta)). 5. **Não dependa da ordem** das chaves de um objeto JSON nem de campos opcionais estarem presentes. 6. **Não derive nada de um ID.** Os IDs são opacos ([Convenções HTTP](../conceitos/convencoes-http.md#identificadores)). #### Eventos de webhook O campo `type` do evento é público e **não tem sufixo de versão** (`deposit.paid`, não `deposit.paid.v2`). A política é a mesma da API: - **mudança aditiva** (campo novo no `data.object`, evento novo): mesma versão. O seu receptor ignora o que não conhece; - **mudança incompatível:** só junto de uma `/v2`, com tipos e envelope próprios, anunciada no Changelog; - **assinatura:** o formato `t=…,v1=…` do cabeçalho `Dracma-Signature` é parte da versão. O receptor deve ignorar partes do cabeçalho que não conhece e aceitar se **qualquer** `v1` conferir, como na rotação de segredo ([Requisição e assinatura](../webhooks/requisicao-e-assinatura.md)). Para o catálogo atual de eventos veja [Catálogo de eventos](../webhooks/catalogo-de-eventos.md). #### Como acompanhar mudanças | Onde | O que mostra | |---|---| | [Changelog](changelog.md) | Mudanças de comportamento visíveis a quem integra, com data | | [OpenAPI](../referencia/openapi.md) | O documento do contrato, versionado; compare com a cópia que você usa na CI | | [Divergências conhecidas](../referencia/divergencias.md) | Onde o documento OpenAPI e o comportamento da API ainda diferem | | [Rotas e campos reservados](../referencia/superficies-reservadas.md) | O que o contrato já declara mas a API ainda não preenche | O documento OpenAPI traz `ETag`: se você o baixa periodicamente, mande `If-None-Match` e receba `304` quando não mudou. Gere um *diff* do arquivo na CI do conector e trate qualquer mudança como motivo para ler o Changelog. #### Depreciação Dentro da v1 nada é removido. Se um dia uma rota, um campo ou um estado precisar sair, ele sai com a `/v2`, e a data é combinada com cada integrador antes de qualquer desligamento, pelo canal informado no onboarding. !!! warning "Não disponível" O Dracma não publica um prazo mínimo fixo de aviso de depreciação. O prazo é combinado por contrato com cada integrador. ### Exemplos Exemplos hipotéticos, só para treinar a classificação: | Mudança | Compatível? | Por quê | |---|---|---| | Um campo que vinha `null` no depósito passa a vir preenchido | Sim | Campo existente ganha valor | | O saque ganha um estado novo | Sim | Valor novo de enumeração aberta; trate como não terminal | | Um evento novo entra no catálogo | Sim | O receptor responde `200` e guarda | | `amount.amount` passa a aceitar decimais | Não | Muda o tipo e o significado de `Money` | | `POST /deposits` passa a exigir `player.email` para todos | Não | Torna obrigatório o que era opcional | | `code` `rate_limited` troca de `429` para `503` | Não | Muda o significado de um código | ### Erros e casos de borda - **Estrito no que você envia, tolerante no que você lê.** A API recusa campo desconhecido no **seu** corpo (`unknown_field`), mas o seu leitor deve ignorar campo desconhecido na **resposta** dela. Mandar um campo "novo" que a documentação ainda não lista dá `400`: não adivinhe campos. - **O OpenAPI pode listar mais do que a API faz.** Campos e rotas do contrato 1.1.0 (câmbio, recebimentos, `recall`, trilhos bancários) estão declarados e ainda não são preenchidos: veja [Rotas e campos reservados](../referencia/superficies-reservadas.md). - **Correção de comportamento não é versão nova.** Quando a API passa a se comportar como o contrato já descrevia (um erro que era `500` e vira `400`, por exemplo), isso é correção e aparece no Changelog, sem mudar o número da versão. - **Mudança de configuração da sua operação** (um método novo habilitado, uma regra mais apertada) não é mudança de versão: consulte `GET /currencies` e `GET /rules`. ### Veja também - [Changelog](changelog.md) - [Convenções HTTP](../conceitos/convencoes-http.md) - [Erros](../api/erros.md) - [Catálogo de eventos](../webhooks/catalogo-de-eventos.md) - [OpenAPI](../referencia/openapi.md) - [Divergências conhecidas](../referencia/divergencias.md) --- # Arquivo: docs/versionamento/changelog.md ## Changelog > Para: Dev · Fin · Adm — Conferido em 2026-10-18. O que mudou no Dracma e é visível para quem integra ou administra uma operação: contrato, comportamento da API, webhooks, painel e acesso. As entradas vão da mais nova para a mais antiga, a partir de 2026-09-27 (a data da edição anterior deste guia). Mudanças internas que não alteram o que você vê não estão aqui. ### O que é Um registro por data, com a área, o efeito para você e o que fazer. Todas as datas estão em UTC e são a data em que a mudança passou a valer no código. Quando uma mudança vale só em operações Teste ou só em operações Real, a entrada diz. A política de compatibilidade está em [Versionamento](politica.md): dentro da v1 só entram acréscimos, e correção de comportamento não muda o número da versão. ### Quando usar - Ao atualizar o conector ou ao investigar um comportamento que mudou ("isso respondia `401`, agora responde `429`"). - Ao revisar o guia: todo o conteúdo foi reconferido em **2026-10-18** contra o código de cada serviço. A edição anterior (2026-09-27) está defasada em pontos que esta página lista: devolução real de saque, lista de IPs, autenticação sem oráculo, limites por jogador, painel completo e e-mail real no hml. - Ao preparar o corte para a operação Real: percorra as entradas e confirme que o seu conector já trata cada uma. ### Como funciona #### Versões do contrato | Versão | O que trouxe | |---|---| | 1.0 | Base: depósitos e saques PIX em BRL, autenticação HMAC, idempotência, webhooks assinados, reconciliação, sandbox. | | 1.0.1 | `review_reasons` no depósito (motivos de `under_review`) e o objeto já existente em `existing` no `409 duplicate_external_id`. | | 1.1.0 | Versão vigente. Declara wire, SWIFT, câmbio e recebimentos bancários, campos novos nos objetos (`fee`, `fees`, `rail`, `fx`, `compliance`, `return`, `recall` e outros) e a regra do **leitor tolerante**. A API implementa só o recorte PIX; o resto é reservado ([Rotas e campos reservados](../referencia/superficies-reservadas.md)). | #### 2026-10-18 | Área | Mudança | O que fazer | |---|---|---| | API | **A lista de IPs da operação deixa de ser aplicada às chamadas.** Nenhuma chamada recebe mais `403 ip_not_allowed`. A tela de IPs do painel continua registrando a lista. | Não use IP como controle de acesso. A autenticação por assinatura e os escopos seguem valendo ([Autenticação HMAC](../api/autenticacao.md)). | | Guia | O guia foi reescrito e reconferido contra o código, e passa a ser publicado em `docs.dracma.cfd`, com o documento OpenAPI ([OpenAPI](../referencia/openapi.md)). Autenticação, erros, idempotência, paginação e limites ganharam páginas próprias. | Atualize favoritos e links. | #### 2026-10-15 | Área | Mudança | O que fazer | |---|---|---| | Painel | **Tenants e operações são criados pelo painel**, pela equipe do Dracma, com acompanhamento do estado da operação: Configurando, Ativa ou Suspensa ([Tenants e operações](../painel/tenants-e-operacoes.md)). | Nada. O primeiro acesso do administrador do integrador é por convite ([Do zero ao go-live](../onboarding/README.md)). | !!! note "Confirmar após deploy" O convite ao administrador do integrador, que vira dono do tenant e cria as próprias operações e chaves, é o fluxo de entrada descrito no onboarding. Confirme o comportamento no seu ambiente antes de contar com ele em um corte. #### 2026-10-12 | Área | Mudança | O que fazer | |---|---|---| | Painel | **Painel completo.** Chaves de API e IPs, usuários e papéis, webhooks (configuração, entregas, DLQ), regras e listas, relatórios com exportação CSV e JSON, alertas, auditoria com trilha verificável, reconciliação e saldos e caixa, cobrança de teste e o pedido de informação ao operador no detalhe do saque. | Veja [O painel](../painel/README.md). | | Painel | **Aprovação por 1 outra pessoa.** Ações que pediam 2 aprovações (dar papel que aprova dinheiro, afrouxar regra, remover entrada de lista, ativar VIP, exportar com dados pessoais) passam a pedir 1, sempre de outra pessoa; quem propõe nunca vota. A mesma pessoa pode administrar o tenant e aprovar dinheiro. Saque marcado para dupla aprovação continua exigindo 2 revisores distintos. | Revise os papéis da sua equipe ([Equipe e papéis](../painel/equipe-e-papeis.md)). | | Painel | Chave de API nova exige aprovação de outra pessoa e espera de 24 h; o segredo é revelado **uma vez**, em janela de 24 h, ao proponente. Chave não revelada nesse prazo é revogada. | [Chaves de API](../painel/chaves-de-api.md). | | Painel | **Cobrança de teste.** Cria um depósito PIX de teste pela tela. Em operação Real exige confirmação explícita, porque o dinheiro é real. | [Cobrança de teste](../painel/cobranca-de-teste.md). | | Operação Real no hml | No ambiente de homologação, a operação **Real** passa a usar o PSP de verdade e **move dinheiro real**. Operações Teste continuam no simulador. | Use a chave `key_test_` para ensaios; só use `key_live_` no hml com valor pequeno e intencional ([Ambientes, tipos de operação e hosts](../conceitos/ambientes-e-hosts.md)). | | Caixa e reconciliação | Depósito `returned` deixa de debitar o caixa da operação por um dinheiro que nunca entrou. O item de devolução na reconciliação usa o valor efetivamente recebido. | Confira a sua conciliação ([Reconciliar com o seu livro](../dinheiro/reconciliacao-do-operador.md)). | | Webhooks | Configuração pelo painel com filtro por tipo de evento e teste a seco; resolução e reentrega de itens na DLQ; alertas do painel por e-mail. | [Webhooks no painel](../painel/webhooks.md). | !!! danger "Dinheiro real" No hml, uma chamada com `key_live_` cobra e paga de verdade. Ensaie com `key_test_`. #### 2026-10-11 | Área | Mudança | O que fazer | |---|---|---| | E-mail | **No hml, os e-mails chegam de verdade** (convites, alertas, códigos de login). Só endereços de teste (`.test`, `.invalid`, `.example`) continuam suprimidos. | Use um endereço real e seu para o administrador; nunca use endereço de terceiros em ensaios. | | Acesso | Login e cadastro em etapas: e-mail, primeiro método e segundo método, um por vez. O link do convite aponta para a tela de cadastro correta. | [Conta, login e dispositivos](../painel/conta-e-login.md). | #### 2026-10-10 | Área | Mudança | O que fazer | |---|---|---| | Saques | **Devolução PIX de saque pago.** O saque `paid` pode virar `returned` quando o banco devolve o PIX, com o objeto `return` (`reason_code` ou `unspecified`, `returned_amount`, que pode ser **parcial**, e `returned_at`). O caixa da operação é creditado pelo valor devolvido. | Trate `returned` no seu livro: credite ao jogador (ou à fila de exceções) só o `returned_amount` ([Ciclo do saque](../dinheiro/ciclo-do-saque.md)). | | Acesso | **Autenticação forte.** Passkey, aplicativo de código (TOTP), código por e-mail, confirmação extra (*step-up*) em ações sensíveis, códigos de recuperação e vários dispositivos. Sessão de 7 dias deslizantes; sair encerra a sessão no servidor. | [Conta, login e dispositivos](../painel/conta-e-login.md). | | Painel | Primeiras telas: operações e saldos, filas de saques e de depósitos em revisão com voto, transações e auditoria. | [O painel](../painel/README.md). | | E-mail | **E-mails do Dracma ao operador:** alerta de entregas de webhook que esgotaram as tentativas, saque em revisão e resumo diário. | Cadastre os endereços de alerta no webhook ([Alertas e e-mails](../painel/alertas.md)). | | Sandbox | Na API de operação Teste do hml, as rotas `/v1/sandbox/*` passam a estar disponíveis. Em produção e em operação Real continuam sendo `404`. | [Sandbox](../sandbox/README.md). | | Segurança | Correção: um `Dracma-Timestamp` absurdamente distante no futuro podia ser aceito por estouro de inteiro; agora é recusado como qualquer valor fora da janela. | Nada. | #### 2026-10-02 | Área | Mudança | O que fazer | |---|---|---| | API | **Rotas do contrato 1.1.0 no recorte PIX.** `GET /fx/quotes/{id}` e `GET /receipts/{id}` respondem `404`; `GET /receipts` devolve página vazia; `POST /withdrawals/{id}/recall` responde `409 invalid_state_transition`; `POST /sandbox/bank-transfers/incoming` responde `400 rail_not_enabled`. Nada move dinheiro. | Nada. Veja [Rotas e campos reservados](../referencia/superficies-reservadas.md). | | Autenticação | **Sem oráculo de existência de chave.** Chave inexistente e assinatura errada voltam a mesma resposta, `401 invalid_signature`. `401 unknown_key` por chave revogada e `403` só aparecem depois de a assinatura de uma chave real conferir. | Em diagnóstico, parta de `invalid_signature` ([Autenticação HMAC](../api/autenticacao.md)). | | Autenticação | **`Dracma-Timestamp` só na forma canônica**: só dígitos, sem sinal, sem espaço, sem zero à esquerda. Valores como `+1790000000` ou `01790000000` voltam `401 timestamp_out_of_window`. O `t=` do webhook também é canônico. | Gere o timestamp com `floor(agora / 1000)` e formate como inteiro simples. | | API | **Validação de JSON mais estrita.** Chave duplicada variando maiúsculas e minúsculas é `400 duplicate_key`; JSON com delimitador trocado deixa de dar `500` e vira `400 validation_failed`. | Não envie chaves repetidas. | | API | Toda resposta `/v1` traz `traceparent`; o `Dracma-Trace-Id` é o ID do *trace*, e o seu `traceparent` de entrada é continuado. | [Convenções HTTP](../conceitos/convencoes-http.md#rastreamento). | #### 2026-09-28 | Área | Mudança | O que fazer | |---|---|---| | Limites | **Saques por jogador: 3 por hora**, além dos 10 depósitos por hora. Estouro: `429 rate_limited` ([Limites e quotas](../api/limites.md#baldes-de-taxa)). | Mostre "tente mais tarde" ao jogador; não faça laço. | | Erros | `500 internal_error` passa a vir com `retryable: true`. | Repita com a mesma `Idempotency-Key`. | | Chaves | A API guarda a chave resolvida por até 30 s: a revogação de uma chave passa a valer em até 60 s em todos os nós. | Revogue e confirme o "último uso" no painel. | | Sandbox | Estorno e *chargeback* de depósito por ação de sandbox (`refund`, `chargeback`). Simulador de saque: valor terminado em **77 centavos** (por exemplo `2077`) fica aceito e pendente até uma ação de sandbox. | [Receitas](../sandbox/receitas.md). | | Contrato | `GET /openapi.yaml` servido pela API, com `ETag`. | [OpenAPI](../referencia/openapi.md). | | Webhooks | **Jitter nas retentativas.** O atraso de cada retentativa é o intervalo do cronograma mais 0% a 10%; nenhuma tentativa sai antes do intervalo base. | Não dependa do horário exato da retentativa ([Retentativas e DLQ](../webhooks/retentativas-e-dlq.md)). | #### Linha de base (2026-09-27) Tudo o que estava descrito na edição anterior deste guia continua valendo, em especial: PIX em BRL para depósito e saque; autenticação HMAC com janela de ±300 s; idempotência de 30 dias; limites de 5 req/s em listagens, 10 depósitos por hora por jogador e 20 falhas de assinatura por minuto por IP; `POST /rules/change-requests`; exigências de depósito por operação em `GET /currencies`; `POST /fx/quotes` respondendo `422 fx_not_enabled`; validação de UTF-8 do corpo. ### Exemplos Como ler uma entrada. A de 2026-10-02 sobre timestamp: 1. **Sintoma possível:** depois de uma atualização de biblioteca, o conector passou a receber `401 timestamp_out_of_window` com o relógio certo. 2. **Causa:** o timestamp saía com `+`, com espaço ou com zero à esquerda (por exemplo `01790000000`). 3. **Correção:** `str(int(agora_em_segundos))`, sem formatação. E a de 2026-10-10 sobre `returned`: se o seu livro só conhecia `paid`, o jogador que teve o PIX devolvido ficaria com o saque "pago" para sempre. Passe a tratar `returned` e o `return.returned_amount`. ### Erros e casos de borda - **hml e produção.** Esta página descreve o comportamento do código. Um ambiente só passa a se comportar assim depois de receber a versão; no hml as mudanças chegam antes. Confira o `GET /v1/ping` e uma chamada de teste antes de contar com uma mudança. - **Datas.** Valem em UTC. Uma mudança de 2026-10-18 pode ter chegado ao seu ambiente depois. - **O que não aparece aqui.** Ajustes de operação do Dracma, regras da sua operação (configuração) e correções que não mudam o que você observa. - **Documento OpenAPI.** Alguns trechos do documento ainda não refletem as mudanças acima; a lista está em [Divergências conhecidas](../referencia/divergencias.md). ### Veja também - [Versionamento](politica.md) - [Autenticação HMAC](../api/autenticacao.md) - [Limites e quotas](../api/limites.md) - [Ciclo do saque](../dinheiro/ciclo-do-saque.md) - [O painel](../painel/README.md) - [Divergências conhecidas](../referencia/divergencias.md) --- # Arquivo: docs/ajuda/README.md ## Ajuda > Para: Dev · Fin · Adm — Conferido em 2026-10-18. Esta seção reúne o que fazer quando algo não funciona ou quando você tem uma dúvida pontual. Comece pelo sintoma: a página certa depende de onde o problema aparece. ### O que é | Página | Para quê | |---|---| | [FAQ](faq.md) | Respostas curtas, agrupadas por tema, com link para a página completa. Inclui o que acontece numa operação suspensa. | | [Problemas de integração](troubleshooting.md) | Roteiro por sintoma para erros de chamada: `401`, `403`, `404`, `409`, `429`, `503` e ambiente errado. | | [Suporte](suporte.md) | Como abrir um chamado e o que enviar. | ### Quando usar Use o quadro abaixo para achar a página mais próxima do seu sintoma. Cada guia de problemas termina em "o que mandar ao suporte". | O que você vê | Vá para | |---|---| | Uma chamada à API volta com erro | [Problemas de integração](troubleshooting.md) e o catálogo em [Erros](../api/erros.md) | | `401` com a assinatura que parece certa | [Passo zero: reproduza os vetores](troubleshooting.md#passo-zero-reproduza-os-vetores) | | O webhook não chega, chega duplicado ou fora de ordem | [Problemas comuns de webhook](../webhooks/troubleshooting.md) | | O saldo, o caixa ou um estado não bate | [Runbook de dinheiro](../dinheiro/runbook.md) | | Uma tela do painel mostra uma mensagem de erro | [Mensagens do painel](../painel/erros-do-painel.md) | | Dúvida sobre ambientes, chaves, IPs, mTLS ou primeiro acesso | [FAQ](faq.md) | | Nada disso resolveu | [Suporte](suporte.md) | ### Como funciona Em qualquer problema, siga a mesma ordem: 1. **Guarde o `Dracma-Trace-Id`** da resposta com erro, o `external_id` e o horário em UTC. Sem eles ninguém reconstrói o que aconteceu. 2. **Confirme o ambiente.** Chame `GET /v1/ping` e veja `environment`. Uma chave de Teste num host de operação Real devolve `401 unknown_key` ([Ambientes, tipos de operação e hosts](../conceitos/ambientes-e-hosts.md)). 3. **Elimine a causa local.** Reproduza os [vetores de assinatura](../exemplos/README.md) e confira o relógio por NTP. 4. **Procure o sintoma** na página da tabela acima. 5. **Abra um chamado** só depois, com os dados do passo 1 ([Suporte](suporte.md)). ### Exemplos Um `503` em laço num saque: o [roteiro de `503`](troubleshooting.md) manda repetir com a mesma `Idempotency-Key` e respeitar o `Retry-After`. Se persistir, o chamado leva o `trace_id` da última resposta e o `external_id` do saque. ### Erros e casos de borda - Nunca envie o segredo da chave, o segredo de webhook nem documento de jogador em um chamado. - Se um segredo vazou, **não espere resposta**: revogue e troque a chave você mesmo ([Segredos e vazamento](../seguranca/segredos-e-credenciais.md#procedimento-de-emergencia-um-segredo-vazou)) e avise o suporte em seguida. ### Veja também - [Início](../README.md) - [Referência rápida](../referencia-rapida.md) - [Glossário](../conceitos/glossario.md) --- # Arquivo: docs/ajuda/faq.md ## FAQ > Para: Dev · Fin · Adm — Conferido em 2026-10-18. Respostas curtas às perguntas mais comuns de quem integra, de quem cuida do financeiro e de quem administra a operação. Cada resposta aponta para a página que explica o assunto por inteiro. ### O que é Um índice de perguntas, agrupado por tema. A pergunta sobre **operação suspensa** (`403 operation_suspended`) tem resposta completa no fim da página, em "Erros e casos de borda". ### Quando usar Quando você tem uma dúvida pontual e quer a resposta antes de ler a página inteira. Para um erro em andamento, comece por [Problemas de integração](troubleshooting.md), que parte do sintoma. ### Como funciona #### Ambientes e acesso **Qual é a diferença entre operação Teste e operação Real?** A operação **Teste** usa o simulador: nenhum dinheiro se move e as rotas `/v1/sandbox/*` existem em homologação. A operação **Real** cobra e paga de verdade, **em qualquer ambiente**, inclusive no hml. A chave diz qual é qual: `key_test_…` para Teste, `key_live_…` para Real. Veja [Ambientes, tipos de operação e hosts](../conceitos/ambientes-e-hosts.md#matriz-instancia-tipo). **Em que operação eu caí?** Chame `GET /v1/ping`: o campo `environment` devolve `sandbox` (Teste) ou `production` (Real). Uma chave do tipo errado para o host volta `401 unknown_key`. **Preciso cadastrar IPs fixos do meu servidor?** Não para as chamadas funcionarem. A tela de IPs registra a lista, mas ela não é aplicada às chamadas hoje; veja [IPs do conector](../painel/allowlist-de-ip.md#ips-do-conector). **mTLS é obrigatório?** Não por padrão. O certificado de cliente é uma camada opcional de rede; a autenticação que vale em toda chamada é a assinatura HMAC. Detalhes em [Rede, lista de IPs e mTLS](../seguranca/rede-allowlist-e-mtls.md). **Posso chamar a API do navegador do jogador?** Não. A API é para o servidor do seu conector: o segredo da chave nunca pode ir para o navegador, e a API não responde a CORS para o seu domínio. **Como o administrador do integrador entra pela primeira vez?** O Dracma cria o tenant e a primeira operação e **convida o administrador por e-mail**. Quem aceita o convite vira dono do tenant e cria as próprias operações e chaves no painel. Veja [Do zero ao go-live](../onboarding/README.md). !!! note "Confirmar após deploy" O fluxo de convite está sendo entregue. Confirme com o Dracma, no onboarding, o e-mail do convite e o prazo de aceite antes de planejar o primeiro acesso. **Perdi o segredo da chave. Dá para ver de novo?** Não. O segredo é exibido **uma única vez**, ao proponente, em uma janela de 24 h depois da aprovação. Crie outra chave e revogue a antiga ([Chaves de API](../painel/chaves-de-api.md)). **Quantas chaves posso ter ao mesmo tempo?** Até 2 chaves ativas por operação, o que permite trocar sem parada: crie a nova, faça o deploy, confira o "último uso" e revogue a antiga. A revogação vale em até 60 s. **Minha URL de webhook em homologação é um domínio meu. Funciona?** Fora de produção, o Dracma só entrega webhook em domínios de teste (`.test`, `.example`, `.invalid`) ou em um domínio que ele tenha liberado para você. No onboarding, informe o domínio que você vai usar e peça a liberação. Endereços temporários de túnel não são aceitos. Veja [Seu endpoint](../webhooks/endpoint-e-politica-de-url.md). #### Chamadas e erros **Meu `POST` deu timeout. O saque (ou o depósito) foi criado?** Você não sabe. Repita com a **mesma** `Idempotency-Key` e o mesmo corpo: se ele já foi criado, você recebe a mesma resposta, com `Idempotent-Replayed: true`; se não, ele é criado agora. Não crie com chave nova. Veja [Idempotência e repetição](../api/idempotencia.md). **Posso repetir um `4xx`?** Não: é uma decisão. Corrija a causa. Para tentar de novo depois de uma recusa de negócio, use `external_id` e `Idempotency-Key` novos. **Recebi `202` ao criar um depósito. Deu erro?** Não. O depósito existe no estado `created`; o PSP ainda não devolveu as instruções de pagamento. Siga pelo webhook e pelo `GET /deposits/{id}`. Saque nunca responde `202`: é sempre `201`. **Por que `403 insufficient_scope`?** A chave não tem o escopo da rota (por exemplo, `read` não cria depósito). Também aparece quando você usa um método que a API não tem (`PUT`, `DELETE`). Veja a tabela em [Autenticação HMAC](../api/autenticacao.md#escopos). **Por que recebo `404` em um depósito que existe?** Porque o objeto é de **outra operação** (outra chave) ou o `id` está errado. A API responde `404` nos dois casos, de propósito, sem confirmar se o objeto existe em outra operação. **Quanto posso chamar por segundo?** Chave de escrita: 50/s, rajada 100; leitura: 100/s por chave; listagens: 5/s. Por jogador: 10 depósitos e 3 saques por hora. Tudo em [Limites e quotas](../api/limites.md#baldes-de-taxa). **Posso mandar `amount` com decimais ou como número?** Não. `amount` é uma string de inteiro na menor unidade (`"5000"` para R$ 50,00). Veja [Convenções HTTP](../conceitos/convencoes-http.md). **O e-mail do jogador é obrigatório no depósito?** Depende da operação: algumas exigem `player.name` e `player.email` (`GET /v1/currencies` lista as exigências em `method_details`). Sem o campo exigido, a resposta é `400` com `reason: required_by_operation` e nada é criado. Em operação Teste, o e-mail precisa ser de domínio reservado (`.test`, `.invalid`, `.example`). **Posso usar cartão, cripto, boleto, wire, SWIFT ou câmbio?** !!! warning "Não disponível" Hoje a API opera **PIX em BRL**. Qualquer outro método responde `400 method_not_enabled`; `POST /fx/quotes` responde `422 fx_not_enabled`. As rotas e os campos do contrato 1.1.0 para esses métodos existem, mas não são preenchidos. Veja [Rotas e campos reservados](../referencia/superficies-reservadas.md). **O que fazer com um `code` ou `status` que não conheço?** Seja um leitor tolerante: trate pela classe HTTP e pelo `retryable`, e trate estado desconhecido como não terminal, conferindo pelo fetch-back. Veja [Versionamento](../versionamento/politica.md#o-seu-lado-leitor-tolerante). #### Dinheiro **O Dracma cobra tarifa?** A tarifa é negociada **por contrato**, e esta documentação não traz valores. O Dracma não desconta tarifa do crédito do jogador nem do valor pago: o campo `fee` do saque vem `null`, e o depósito não o devolve. Veja [Tarifas](../dinheiro/tarifas.md). !!! warning "Não disponível" Não há extrato de tarifas nem relatório de uso por API. Pergunte ao seu contato comercial. **Como coloco dinheiro no caixa da operação para pagar saques?** !!! warning "Não disponível" Não existe aporte de caixa pela API nem pelo painel. O caixa da operação cresce com **depósitos pagos** e com **devoluções** de saque. Saque acima do caixa livre responde `422 insufficient_operation_balance`. Veja [Saldos e caixa livre](../dinheiro/saldos.md). **O saque ficou em `processing` por muito tempo. É erro?** Não necessariamente. O estado só muda quando o banco confirma. Não crie um saque novo nem devolva a reserva ao jogador sem a resposta conclusiva. Veja [Ciclo do saque](../dinheiro/ciclo-do-saque.md). **O banco devolveu o PIX de um saque. O que muda?** O saque `paid` pode virar `returned`, com o objeto `return` (`reason_code`, `returned_amount`, `returned_at`). A devolução pode ser **parcial**. O caixa da operação é creditado pelo valor devolvido. Veja [Devoluções, estornos e disputas](../dinheiro/devolucoes-estornos-e-disputas.md). **Como confiro o meu livro com o do Dracma?** Com `GET /v1/reconciliation` (um dia por moeda, disponível a partir de 02:00 UTC do dia seguinte) e `GET /v1/balances`. Veja [Reconciliar com o seu livro](../dinheiro/reconciliacao-do-operador.md). #### Webhooks e eventos **Perdi webhooks. Como recupero?** Com `GET /v1/events?after=<último evt_ processado>`, que devolve os eventos da sua operação em ordem de criação. Veja [Recuperação por `GET /events`](../webhooks/recuperacao-get-events.md). **Posso confiar no corpo do webhook para creditar o jogador?** Não. O corpo é um aviso. Antes de mover dinheiro, consulte o objeto (`GET /deposits/{id}` ou `GET /withdrawals/{id}`) e confira o `external_id`. Veja [Receptor de webhooks](../webhooks/receptor.md). #### Painel e equipe **Por que minha ação no painel ficou "pendente"?** Ações sensíveis (criar chave, mudar webhook, afrouxar regra, dar papel que aprova dinheiro) exigem a aprovação de **outra pessoa** e, em alguns casos, uma espera de 24 h. Quem propõe não vota. Veja [Equipe e papéis](../painel/equipe-e-papeis.md). **A operação aparece como "Suspensa" no painel. Posso reativá-la?** Não pelo painel. Veja a resposta completa abaixo. ### Exemplos Mensagem boa para o suporte (copie e preencha; todos os campos vêm da resposta do erro e do seu log): ```text Operação: Tipo: Teste (key_test_) ou Real (key_live_) Quando (UTC): 2026-10-18T15:04:05Z Chamada: POST /v1/withdrawals Resposta: 503 service_unavailable Dracma-Trace-Id: 4bf92f3577b34da6a3ce929d0e0e4736 external_id: wdr_0192a3b4-5c6d-7e8f-9a0b-1c2d3e4f5a6b O que já tentei: repeti 3 vezes com a mesma Idempotency-Key, com backoff ``` Nunca envie o segredo da chave, o segredo de webhook nem o corpo com documento do jogador. O `trace_id`, o `external_id` e o horário bastam. O canal de suporte está em [Suporte](suporte.md). ### Erros e casos de borda #### Operação suspensa: `403 operation_suspended` **O que acontece.** Com a operação suspensa, **toda** chamada `/v1` autenticada volta `403 operation_suspended`, inclusive `GET` e `GET /v1/ping`. A assinatura continua sendo conferida: com assinatura inválida você ainda vê `401`. **Quem suspende e quem retoma.** O Dracma. O painel mostra a situação **Suspensa** na lista de operações, mas não tem ação para suspender nem para retomar. Fale com o suporte ([Suporte](suporte.md)), informando o `operation_id` e o `Dracma-Trace-Id` da resposta. **O que fazer no conector enquanto durar:** 1. **Pare de enviar**: um `403` é definitivo para aquela requisição. Não faça laço de repetição. Abra o circuito do conector e pause a fila de depósitos e saques. 2. **Mantenha as reservas.** Saque cujo `POST` você enviou e cuja resposta não viu continua com a reserva do jogador onde está. Não a devolva às cegas. 3. **Não crie objetos novos** com `external_id` novo para "contornar": quando a operação voltar, o pedido original ainda pode estar válido. 4. **Objetos já criados seguem o próprio ciclo.** Um depósito pode ser pago ou expirar, e um saque em curso pode avançar, porque isso não depende das chamadas à API. 5. **Depois da retomada**, recupere o que aconteceu: `GET /v1/events?after=<último evento processado>` e, para cada pedido incerto, `GET /v1/deposits?external_id=…` ou `GET /v1/withdrawals?external_id=…`. Só então retome a fila, começando devagar por causa do [limite de taxa](../api/limites.md). **Como saber que voltou.** `GET /v1/ping` volta `200`. Faça esse ping a cada minuto, com a assinatura de sempre, em vez de repetir chamadas de negócio. #### Outras situações - **`403 insufficient_scope` logo depois de criar a chave:** confira os escopos marcados na criação; se faltar um, crie uma chave com os escopos certos e revogue a anterior. - **`401 unknown_key` com a chave certa:** o tipo da chave não bate com o host (Teste × Real), ou a chave foi revogada. - **Divergência entre o guia e a API:** a API vale; registre em [Divergências conhecidas](../referencia/divergencias.md) e avise o suporte. ### Veja também - [Problemas de integração](troubleshooting.md) - [Suporte](suporte.md) - [Idempotência e repetição](../api/idempotencia.md) - [Erros](../api/erros.md) - [Limites e quotas](../api/limites.md) - [Ciclo do saque](../dinheiro/ciclo-do-saque.md) - [Ambientes, tipos de operação e hosts](../conceitos/ambientes-e-hosts.md) --- # Arquivo: docs/ajuda/troubleshooting.md ## Problemas de integração > Para: Dev — Conferido em 2026-10-18. Roteiro de diagnóstico para os problemas mais comuns ao chamar a API: `401` por relógio ou corpo, `403`, `404`, `409`, `429` e `503` em laço, e ambiente errado. Parta do sintoma, confirme a causa e aplique a correção. ### O que é Cada seção começa pelo que você vê (o status e o `code`) e termina no que fazer. A API confere as coisas **numa ordem fixa** ([Autenticação HMAC](../api/autenticacao.md#ordem-de-verificacao)), e o primeiro problema é o que volta: use isso para eliminar causas. Problemas de webhook estão em [Problemas comuns de webhook](../webhooks/troubleshooting.md); problemas de dinheiro (saldo, estado parado), em [Runbook de dinheiro](../dinheiro/runbook.md). ### Quando usar Quando uma chamada falha e você não sabe por quê, e antes de abrir um chamado. Guarde o `Dracma-Trace-Id` de toda resposta que não seja `2xx`: com ele o suporte acha a requisição. ### Como funciona #### Passo zero: reproduza os vetores Antes de qualquer outra hipótese, faça o **seu** código produzir os três vetores de [`vetores-assinatura.json`](../exemplos/vetores-assinatura.json) (mesma assinatura, byte a byte). Se não produz, o problema é local e não de rede nem do Dracma. Imprima o pedido canônico com `cat -A` ou equivalente: um `$` no fim da última linha é um `\n` que não pode existir. #### `401`: autenticação ```mermaid flowchart TD A[401] --> B{code} B -->|timestamp_out_of_window| C[Relógio, unidade ou formato do timestamp] B -->|invalid_signature| D[Segredo, key_id, bytes do corpo, caminho ou query] B -->|replayed_nonce| E[Nonce repetido, inclusive em retry] B -->|unknown_key| F[Tipo da chave x host, ou chave revogada] ``` **`timestamp_out_of_window`** | Causa provável | Como confirmar | Correção | |---|---|---| | Relógio do servidor fora de ±300 s | Compare o cabeçalho `Date` da resposta (vem também no erro) com `date -u` no seu servidor | Ative o NTP (`chronyc tracking`); alerte desvio acima de 5 s | | Timestamp em milissegundos | O valor tem 13 dígitos | `floor(agora_ms / 1000)` | | Formato fora do canônico | Tem `+`, espaço, zero à esquerda, casa decimal ou notação científica | Inteiro simples, sem formatação | | Assinou com um timestamp e enviou outro | O cabeçalho difere do usado no canônico | Calcule uma vez e reuse na assinatura e no cabeçalho | | Relógio do contêiner diferente do host (VM pausada, snapshot restaurado) | O desvio aparece só em uma máquina | Sincronize ao restaurar | **`invalid_signature`**: é o código mais genérico, de propósito (o Dracma não revela se a chave existe). | Causa provável | Como confirmar | Correção | |---|---|---| | Segredo errado, de outra chave, ou com `\n` ou espaço no fim | Reproduza o vetor com o segredo lido do seu cofre | Leia o segredo sem a quebra de linha; confira o `key_id` | | `Dracma-Key-Id` digitado errado | Chave "inexistente" e assinatura errada voltam o mesmo `401` | Copie o `key_id` do painel | | Corpo diferente do assinado | Só `POST` falha; reserializou o JSON, mudou a ordem das chaves ou o espaçamento | Serialize uma vez; assine e envie os mesmos bytes | | Biblioteca HTTP recodifica o corpo (compressão, `charset`) | Compare o `sha256` do corpo no seu log com o do pedido de rede (proxy de depuração) | Envie o corpo cru, sem compressão | | Caminho sem `/v1`, ou com host na frente | O canônico tem `/deposits` em vez de `/v1/deposits` | Assine o caminho completo | | Query reordenada ou decodificada | Só `GET` com parâmetros falha | Assine a URL exatamente como enviada (`curl --path-as-is`) | | Método em minúsculas | O canônico tem `post` | Maiúsculas | | Nonce com maiúscula ou com menos de 32 caracteres | O nonce não casa `^[0-9a-f]{32}$` | `openssl rand -hex 16` ou equivalente | | Segredo usado decodificado (hexadecimal ou base64) | O vetor só passa decodificando | Use os bytes da própria string | | Um proxy ou balanceador seu muda o caminho ou o corpo | A assinatura passa em chamada direta e falha atrás do proxy | Assine o que **sai** do proxy ou desligue a reescrita | Sobre `429` no lugar de `401`: depois de 20 falhas de assinatura por minuto do mesmo IP, o Dracma passa a responder `429 rate_limited`. Pare o laço, corrija a causa e espere o `Retry-After`. **`replayed_nonce`** | Causa provável | Correção | |---|---| | O *retry* reutiliza o nonce da primeira tentativa | Gere o nonce **dentro** da função que monta cada tentativa | | Dois processos usam um gerador com a mesma semente | Use o gerador do sistema (`crypto/rand`, `random_bytes`) | | O mesmo pedido é enviado duas vezes por um proxy de repetição | Desligue o *retry* automático do proxy e deixe a repetição para o conector | **`unknown_key`** | Causa provável | Como confirmar | Correção | |---|---|---| | Tipo da chave não bate com o host (`key_test_` na API Real, ou o contrário) | O prefixo da chave e o host da sua base | Use o par chave-host certo ([Ambientes, tipos de operação e hosts](../conceitos/ambientes-e-hosts.md#matriz-instancia-tipo)) | | Chave revogada | O painel mostra a chave como Revogada | Crie outra | | `Dracma-Key-Id` ausente ou mal formado | O cabeçalho some em algum proxy | Verifique os cabeçalhos que chegam | #### `403`: autorização Um `403` quer dizer que a **autenticação passou**: a assinatura está certa. | `code` | Causa | Correção | |---|---|---| | `insufficient_scope` | A chave não tem o escopo da rota; ou você usou um método que a API não tem (`PUT`, `DELETE`, `OPTIONS`); ou fez `POST` em rota que não existe | Compare a rota com a tabela de escopos; use a chave do componente certo | | `operation_suspended` | Operação suspensa pelo Dracma | [FAQ](faq.md): operação suspensa | | `ip_not_allowed` | Existe no catálogo; a lista de IPs não é aplicada hoje | Você não deve vê-lo; se vir, abra chamado com o `trace_id` | #### `400`, `404`, `409` e `422` | Sintoma | Causa | Correção | |---|---|---| | `400 validation_failed` com `unknown_field` | Campo que o schema não tem (inclusive `status`, `version`, `paid_amount`) | Remova o campo; a API não aceita campo "extra" | | `400` com `field: Idempotency-Key` | `POST` sem `Idempotency-Key`, ou com menos de 16 caracteres | 16 a 128 caracteres `[A-Za-z0-9_.:-]` | | `400` com `field: amount` | Valor como número, decimal ou com zeros à esquerda | `Money` ([Convenções HTTP](../conceitos/convencoes-http.md#dinheiro-money)) | | `400` com `reason: required_by_operation` | A operação exige um campo que o contrato deixa opcional | Mande o campo; consulte `GET /currencies` | | `400` com `reason: reserved_domain_required` | E-mail de jogador real em operação Teste | Use `.test`, `.invalid` ou `.example` | | `404 not_found` em objeto que existe | O objeto é de outra operação, ou o `id` tem outro formato | Confira a chave e o `id` | | `404` em rota que existe | `GET` em rota só `POST`, ou caminho sem `/v1` | Método e caminho | | `404` **sem corpo** | Caminho fora de `/v1/*`, ou `/v1/sandbox/*` em operação Real | Corrija a base; sandbox só em Teste | | `409 idempotency_key_reused` | Mesma chave com outro corpo, rota ou query | Reenvie os bytes originais, ou use chave nova **e** `external_id` novo (operação nova) | | `409 duplicate_external_id` | O `external_id` já existe | Leia `existing`; concilie em vez de criar outro | | `409 invalid_state_transition` | O estado atual não permite a ação (cancelar saque já enviado, por exemplo) | `GET` do objeto e decida | | `422` | Recusa de negócio (limite, lista, documento, caixa) | [Erros](../api/erros.md); não repita igual | #### `429` e `503` em laço Um laço de repetição sem espera é a causa mais comum de `429` e piora o `503`. 1. **Leia o `Retry-After`** e espere esse tempo, no mínimo. Sem o cabeçalho, use backoff exponencial com *jitter* (1 s, 2 s, 4 s, até 60 s). 2. **Descubra qual balde estourou.** Escrita (50/s, rajada 100), leitura (100/s por chave), listagem (5/s) ou jogador (10 depósitos e 3 saques por hora) — [Limites e quotas](../api/limites.md#baldes-de-taxa). `Retry-After` de minutos é balde de jogador. 3. **Ponha uma fila e um teto de concorrência** no conector: dispare a uma taxa abaixo do teto, nunca em rajada de lote. 4. **Use um disjuntor.** Depois de N falhas seguidas, pare por um intervalo e teste com `GET /v1/ping` antes de voltar. 5. **Mantenha a mesma `Idempotency-Key`** em toda repetição de um `POST`. `429` e `5xx` liberam a chave; a repetição nunca duplica. 6. **`503` repetido por minutos** é problema do Dracma ou de uma dependência dele: junte o `Dracma-Trace-Id` de duas ou três respostas e abra chamado ([Suporte](suporte.md)). Não crie objetos novos para "driblar": consulte por `external_id` ([Idempotência e repetição](../api/idempotencia.md#recuperar-um-post-perdido)). 7. **`503` com `Retry-After: 5` em operação Teste** pode ser a queda simulada ligada por `POST /sandbox/outage`: desligue com `{"down": false}`. #### Ambiente errado Sintomas típicos: `401 unknown_key` com a chave certa; `404` em `/v1/sandbox/...`; depósito que "não aparece" no painel; ou dinheiro movendo quando você achava que era teste. 1. Chame `GET /v1/ping` e leia `environment`: `sandbox` é operação Teste; `production` é operação Real. 2. Compare com o prefixo da chave (`key_test_` ou `key_live_`) e com o host da sua base ([Ambientes, tipos de operação e hosts](../conceitos/ambientes-e-hosts.md)). 3. Confira se o painel que você olha é o do mesmo ambiente (hml e produção são painéis distintos). 4. Guarde **uma** configuração por ambiente (base, `key_id`, segredo, segredo de webhook) e nunca misture. !!! danger "Dinheiro real" Operação Real cobra e paga de verdade, inclusive no hml. Se você não tem certeza do tipo da chave, rode `GET /v1/ping` antes de qualquer `POST`. #### Timeouts e conexão - **Timeout de cliente: 10 s.** O depósito pode levar cerca de 7 s e voltar `202`; o saque é sempre `201`. - **Timeout não é falha.** Repita com a mesma `Idempotency-Key`. Se desistir, consulte por `external_id` antes de decidir. - **Resposta que não é JSON** (página de erro de proxy, `413` sem corpo) vem da entrada pública ou da rede: trate como erro de transporte. - **TLS:** use TLS 1.2 ou superior e valide o certificado do host; não desligue a verificação para "fazer funcionar". ### Exemplos Checagem rápida de relógio, antes de mexer em qualquer outra coisa (a resposta de erro também traz `Date`): ```bash curl -sS -o /dev/null -D - "$DRACMA_BASE_URL/ping" | grep -i '^date:' date -u ``` Se as duas horas diferirem em mais de alguns segundos, o relógio é o problema. Diagnóstico de assinatura com o shell, usando as funções de [`exemplos/shell/lib.sh`](../exemplos/shell/lib.sh): ```bash source exemplos/shell/lib.sh dracma_canonico 1790000000 0123456789abcdef0123456789abcdef POST /v1/deposits corpo.json | cat -A dracma_assinar "$(cat segredo.txt)" 1790000000 0123456789abcdef0123456789abcdef POST /v1/deposits corpo.json ``` Compare as saídas com o vetor 1. Qualquer `$` no fim da última linha do canônico, ou uma assinatura diferente, mostra onde está a diferença. ### Erros e casos de borda - **Funciona direto e falha atrás de um proxy ou API gateway seu:** o proxy reescreve caminho, query ou corpo depois de você assinar. Assine no último salto. - **Funciona em `GET` e falha em `POST`:** quase sempre o corpo assinado não é o enviado, ou falta `Idempotency-Key`. - **Funciona uma vez e depois dá `replayed_nonce`:** nonce fixo ou semente repetida. - **Intermitente, com `timestamp_out_of_window`:** relógio com deriva ou VM pausada. - **Só `401` na primeira chamada do dia:** chave rotacionada e segredo antigo no cofre. - **Depois de 20 falhas, tudo vira `429`:** o IP está bloqueado por falhas de assinatura; corrija a causa e espere. - **Nada disso:** abra um chamado com `Dracma-Trace-Id`, horário em UTC, `operation_id`, rota e `code`. Não envie o segredo da chave. ### Veja também - [Autenticação HMAC](../api/autenticacao.md) - [Erros](../api/erros.md) - [Limites e quotas](../api/limites.md) - [Idempotência e repetição](../api/idempotencia.md) - [FAQ](faq.md) - [Suporte](suporte.md) - [Problemas comuns de webhook](../webhooks/troubleshooting.md) --- # Arquivo: docs/ajuda/suporte.md ## Suporte > Para: Dev · Fin · Adm — Conferido em 2026-10-18. Como falar com o Dracma quando o guia não resolve: por qual canal, quem abre o chamado e o que enviar para que a primeira resposta já seja útil. ### O que é O canal de suporte é **informado no onboarding**, junto com o tenant e a operação Teste. Este guia não publica endereço de e-mail nem telefone: use o canal que o Dracma entregou a você. Quem abre o chamado é, em geral, o Administrador do tenant ou a pessoa de contato que você combinou no onboarding. !!! warning "Não disponível" O Dracma não publica prazos de atendimento (SLA), janelas de manutenção nem uma página de status. O que se aplica a você está no seu contrato e no que foi combinado no onboarding. Não planeje o seu conector contando com um prazo de resposta: ele precisa continuar seguro enquanto espera (veja [Idempotência e repetição](../api/idempotencia.md) e [Política de exceções de dinheiro](../conector/politica-de-excecoes.md)). ### Quando usar Abra um chamado quando: - você seguiu [Problemas de integração](troubleshooting.md) e o sintoma continua; - o `trace_id` de uma resposta `500 internal_error` se repete; - o caixa ou um estado não bate com o que as páginas de [Dinheiro](../dinheiro/README.md) explicam; - é preciso uma ação que só o Dracma faz: criar tenant ou operação, ativar gateway, liberar domínio de webhook, suspender ou retomar uma operação, apagar ou anonimizar dado de jogador; - você suspeita de uso indevido de uma chave ou de acesso ao painel. Antes de abrir, tente o que depende só de você: o [FAQ](faq.md), o [Runbook de dinheiro](../dinheiro/runbook.md), os [Problemas comuns de webhook](../webhooks/troubleshooting.md). ### Como funciona #### O que enviar Mande sempre estes itens. Eles bastam para localizar a requisição nos registros do Dracma. | Dado | De onde | |---|---| | `operation_id` e se a operação é Teste ou Real | resposta de `GET /v1/ping`, ou a tela da operação no painel | | `Dracma-Trace-Id` (ou `trace_id` do corpo) | cabeçalho da resposta com erro; **guarde o de toda resposta que não seja `2xx`** | | Horário em UTC e o fuso da sua leitura | log do conector | | Método, caminho e `code` da resposta | log do conector | | `external_id` e o id do objeto (`dep_...` ou `wdr_...`) | seu banco ou o corpo do evento | | `Dracma-Event-Id` (`evt_...`), se o problema é de webhook | cabeçalho do webhook ou campo `id` do corpo | | O que o seu código fez e o que você já tentou | log e anotações | **Nunca envie:** o segredo da chave, o segredo de webhook, a assinatura completa (se precisar mostrar uma, use só os 8 primeiros caracteres do `v1`), o corpo com documento do jogador, a chave PIX em claro. Exemplo de mensagem: ```text Assunto: saque wdr_0192a3b4... voltou 503 repetidas vezes (operação Real) operation_id: 0198b7c2-3d4e-7f50-8a61-92b3c4d5e6f7 (Real) Dracma-Trace-Id: 4bf92f3577b34da6a3ce929d0e0e4736 external_id: saque-10482 Horário: 2026-10-18 14:05 UTC (três tentativas em 90 s) Resposta: 503 service_unavailable O que já tentei: repeti com a mesma Idempotency-Key e backoff; GET /v1/ping responde 200. ``` #### Como dizer a urgência O Dracma não define severidades publicadas. Diga, com palavras suas, qual destas situações é a sua, para o chamado ser lido na ordem certa: | Situação | Como descrever | |---|---| | Dinheiro se movendo de forma errada, ou segredo vazado | "dinheiro em risco" ou "segredo vazado" no assunto, com o `operation_id` | | Todas as chamadas de uma operação falham | "operação parada" e o `code` que você recebe | | Um objeto parado ou uma divergência de conciliação | o id do objeto e a data da divergência | | Dúvida de configuração ou de uso | a pergunta e a página do guia que você leu | Para as duas primeiras linhas, faça primeiro a sua parte: em vazamento, [revogue e troque a chave](../seguranca/segredos-e-credenciais.md#procedimento-de-emergencia-um-segredo-vazou); com o dinheiro em risco, [suspenda o saque automático](../painel/regras-e-listas.md) no painel. #### O que esperar O suporte consulta os registros pelo `Dracma-Trace-Id`, investiga e orienta. O que o suporte pode fazer em cada caso depende do caso: as páginas de [Dinheiro](../dinheiro/README.md) dizem o que é seu (por exemplo, a decisão sobre um estorno) e o que passa pelo Dracma. Chamado não substitui a conferência diária do [seu livro](../dinheiro/reconciliacao-do-operador.md). ### Exemplos Chamado de webhook que não chega: ```text Assunto: webhook deposit.paid não chegou (operação Teste) operation_id: 0198b7c2-3d4e-7f50-8a61-92b3c4d5e6f7 (Teste) Dracma-Event-Id: evt_e12d177892007213962d29c4b3b60580 Objeto: dep_0198b7c24d5e7a1b8c2d3e4f5a6b7c8d, external_id pedido-10482 Horário: 2026-10-18 14:05 UTC Status da última tentativa no painel: timeout O que já tentei: GET /v1/events?object_id=... mostra o evento; o receptor responde 200 ao curl local. ``` ### Erros e casos de borda - **Chamado sem `Dracma-Trace-Id`.** O suporte precisa pedir de novo. Registre o `trace_id` de toda resposta de erro no log do conector ([Alertas do conector](../conector/alertas-e-observabilidade.md)). - **Não sei o canal.** Peça à pessoa que conduziu o seu onboarding. O Administrador do tenant tem o contato. - **Operação suspensa (`403 operation_suspended`).** Só o Dracma suspende e retoma. Informe o `operation_id` e o `Dracma-Trace-Id` da resposta ([FAQ](faq.md)). - **O chamado pede uma ação que você mesmo faz no painel.** Chaves, equipe, regras e webhooks têm tela própria: veja [O painel](../painel/README.md). ### Veja também - [Ajuda](README.md) - [Problemas de integração](troubleshooting.md) - [Problemas comuns de webhook](../webhooks/troubleshooting.md) - [Runbook de dinheiro](../dinheiro/runbook.md) - [Segredos e vazamento](../seguranca/segredos-e-credenciais.md) --- # Arquivo: docs/exemplos/README.md ## Exemplos executáveis > Para: Dev — Conferido em 2026-10-18. Clientes mínimos, só com biblioteca padrão, em Go, Node, PHP e shell: assinam chamadas, verificam webhooks, calculam o digest da reconciliação e, nos helpers novos, repetem com backoff, leem erros `problem+json`, percorrem listagens e recuperam eventos. Todos têm testes que você pode rodar. ### O que é São exemplos didáticos: copie, adapte à sua linguagem e mantenha os testes passando. Eles não são um SDK. 1. **Assinam** chamadas operador para Dracma (pedido canônico mais HMAC-SHA256, [Autenticação](../api/autenticacao.md#pedido-canonico-e-assinatura)). 2. **Verificam** webhooks Dracma para operador (`t=`, um ou mais `v1`, janela de 300 s, [Requisição e assinatura](../webhooks/requisicao-e-assinatura.md#o-header-dracma-signature)). 3. **Calculam** o digest da reconciliação e verificam a assinatura dele ([Reconciliação](../referencia/reconciliacao.md)). 4. **Repetem** chamadas com backoff, mesma chave e mesmos bytes. 5. **Leem** erros `problem+json`, inclusive respostas da borda sem corpo. 6. **Percorrem** listagens por cursor opaco. 7. **Recuperam** eventos perdidos com `GET /v1/events` (poller). Cada linguagem tem um teste que lê [`vetores-assinatura.json`](vetores-assinatura.json), cópia fiel dos vetores normativos do contrato, e os reproduz **byte a byte**. Antes de ir para produção, o seu código tem de reproduzir os mesmos vetores, inclusive o negativo. Os negativos de timestamp não canônico (`+ts`, `0ts`, espaço, `1e9`, `-0`) estão em [`vetores-assinatura-ts-negativos.json`](vetores-assinatura-ts-negativos.json): a API recusa todos com `401 timestamp_out_of_window`. O `op_nero_hml` que aparece nos vetores é um identificador fictício, escolhido só para os bytes dos vetores serem estáveis; não é uma operação real. | Pasta | Conteúdo | Teste | |---|---|---| | [`go/`](go/) | Pacote `dracma`: `assinatura.go`, `webhook.go` e, novos, `retry.go`, `problema.go`, `paginacao.go`, `eventos.go`. Mais `cmd/ping` (GET `/v1/ping` assinado que mede o desvio de relógio) | `cd go && go test ./... && go vet ./...` | | [`node/`](node/) | `dracma.ts` (TypeScript que o Node 22.18 ou mais novo executa direto, sem build nem dependência) e, novos, `retry.ts`, `problema.ts`, `paginacao.ts`, `eventos.ts` | `cd node && node --test "*.test.ts"` | | [`php/`](php/) | `Dracma.php` (classe final, PHP 8.1 ou mais novo, extensões `hash` e `curl`) e, novos, `Http.php`, `Problema.php`, `Retry.php`, `Paginacao.php` | `cd php && php vetores_test.php && php auxiliares_test.php` | | [`shell/`](shell/) | `lib.sh`, `assinar.sh`, `verificar-webhook.sh`, `vetores.sh` e, novos, `helpers.sh`, `cpf-teste.sh` | `bash shell/vetores.sh && bash shell/helpers_test.sh` | ### Quando usar - Para implementar o seu conector: porte a lógica, não invente outra forma de montar o pedido canônico. - Para depurar uma assinatura que dá `401`: compare, passo a passo, com `vetores.sh` e com `SO_HEADERS=1 bash shell/assinar.sh`. - Para provar o seu conector em CI: rode os mesmos vetores e acrescente os seus testes de repetição e de deduplicação. ### Como funciona #### Executado nesta máquina (última rodada: 2026-10-18) | Exemplo | Runtime | Resultado | |---|---|---| | Go | go1.27.1 linux/amd64 (o módulo declara `go 1.22`) | `go test -count=1 ./...` verde (19 testes de nível superior, vetores e helpers); `go vet` limpo | | Node (TypeScript) | Node v26.11.1 | `node --test "*.test.ts"`: 14 de 14 passam (5 dos vetores, 9 dos helpers) | | PHP | PHP 8.3.35 (imagem oficial `php:8.3-cli-alpine`) | `vetores_test.php`: 28 verificações, todas passam. `auxiliares_test.php`: 42 verificações, todas passam | | Shell | GNU bash 5.3, OpenSSL 3.6.5, jq 1.8.2 (também testado o caminho só com python3) | `vetores.sh`: 24 verificações. `helpers_test.sh`: 42 verificações, todas passam | Os testes dos helpers também foram vistos **falhar**: com a regra de repetição trocada para "repetir qualquer 4xx", e com o poller gravando o cursor **antes** de aplicar o evento, os testes de Go, Node, PHP e shell ficam vermelhos. O que os testes dos **vetores** cobrem, em todas as linguagens: - os 3 vetores de chamada: `sha256_corpo`, `pedido_canonico` e `Dracma-Signature` idênticos; - o webhook válido é aceito; - o webhook de rotação é aceito com o segredo **atual** e também só com o **anterior** (o segundo `v1` do vetor é de fato o HMAC do segredo anterior); - o vetor negativo (corpo alterado em 1 byte) é recusado; - `t=` a +301 s ou -301 s do relógio é recusado e a +300 s é aceito; - `Dracma-Timestamp` diferente do `t=` é recusado; - `v1` com hex inválido, curto ou vazio não confere e não gera exceção; - digest: linhas ordenadas por `object_id` e, no empate (o mesmo objeto em dois itens, por exemplo `deposit` e `refund`), por `kind`, como o Dracma faz; dia vazio é o sha256 de `""`; assinatura aceita e digest adulterado recusado; - o nonce tem 32 hex minúsculos. #### Variáveis de ambiente Estas são variáveis do **seu** processo cliente, lidas pelos exemplos. | Variável | Uso | |---|---| | `DRACMA_BASE_URL` | Base **com** `/v1`. O padrão dos exemplos é um endereço fictício: defina sempre com o host que você recebeu no onboarding ([Ambientes, tipos de operação e hosts](../conceitos/ambientes-e-hosts.md)) | | `DRACMA_KEY_ID` | `key_test_...` (operação Teste) ou `key_live_...` (Real) | | `DRACMA_KEY_SECRET_FILE` | Caminho de um arquivo com o segredo da chave. Permissão `0400`, dono igual ao usuário do serviço, fora do repositório | | `DRACMA_WEBHOOK_SECRETS_FILE` | Arquivo com os segredos de webhook, **um por linha**: o atual e, durante a rotação, o anterior | O segredo **nunca** fica no código, no repositório, em variável de build de frontend nem em log. Os únicos segredos literais aqui são os dos vetores públicos (`sk_test_vetor_...`, `whsec_test_vetor_...`), que não abrem nada. #### Armadilhas (cada uma já custou um `401` a alguém) 1. **O path assinado inclui `/v1`.** Com base `.../v1`, o path de `POST /deposits` é `/v1/deposits`. Os exemplos tiram o path da URL final (`req.URL.RequestURI()`, `url.pathname + url.search`, `parse_url`) e evitam o erro. 2. **Query exatamente como enviada.** Não reordene, não decodifique `%XX` e não troque `+` por `%20` depois de assinar. Monte a URL uma vez e assine essa string. No curl, use `--path-as-is`. 3. **Assine os mesmos bytes que envia.** Serialize o JSON **uma vez**, guarde os bytes, calcule o sha256 sobre eles e envie esses mesmos bytes. No webhook, o HMAC é sobre o corpo **cru** recebido, antes de `JSON.parse` ou `json_decode`. 4. **Nonce em hex minúsculo**, 32 caracteres, de CSPRNG. Hex maiúsculo é recusado com `401 invalid_signature`. Use um nonce novo por requisição, **inclusive ao repetir** um `POST`. 5. **Timestamp em segundos.** Milissegundos (`Date.now()`) caem em `401 timestamp_out_of_window`. 6. **Relógio.** A janela é de ±300 s. Use NTP (chrony com 3 ou mais fontes). Chame `GET /v1/ping` periodicamente: `go run ./cmd/ping` compara `server_time` com o relógio local e **alerta com desvio acima de 5 s**. 7. **Repetição de `POST`.** Em `5xx`, `429` ou erro de rede, repita com a **mesma** `Idempotency-Key` (e timestamp, nonce e assinatura novos). Nunca gere chave nova para "tentar de novo" um `POST` de dinheiro. `4xx` não se repete ([Idempotência e repetição](../api/idempotencia.md)). 8. **Comparação em tempo constante.** Os exemplos usam `hmac.Equal`, `crypto.timingSafeEqual` (com checagem de tamanho) e `hash_equals`. O `lib.sh` do shell **não** tem comparação em tempo constante e passa o segredo como argumento do `openssl`, que fica visível em `ps`. Use o shell só para depurar. 9. **A verificação da assinatura não basta.** Ela cobre só os itens 1 a 3 do webhook. Depois: `Dracma-Event-Id` igual ao `id` do corpo (senão `400`), dedup por `Dracma-Event-Id` e **fetch-back antes de mover dinheiro** ([Receptor passo a passo](../webhooks/receptor.md#os-13-passos-nesta-ordem)). #### Helpers de resiliência Quatro blocos, com a mesma API conceitual nas quatro linguagens. **1. Repetição com backoff.** Repete só o que se pode repetir: erro de rede, `429`, qualquer `5xx` e todo problema com `retryable: true` (inclui `409 idempotency_in_progress`). Não repete `400`, `401`, `403`, `404`, `422` nem os `409` `duplicate_external_id` e `idempotency_key_reused`. A espera é a **maior** entre o `Retry-After` do servidor e um backoff exponencial com jitter (1 s, 2 s, 4 s... até 60 s), limitada a 60 s. Cada tentativa é uma requisição nova (nonce e timestamp novos), com os mesmos bytes e a mesma `Idempotency-Key`. Esgotou: o erro diz que **não** se sabe se o objeto foi criado, e a próxima ação é consultar por `external_id`. **2. Leitor de `problem+json`.** Lê `code`, `title`, `detail`, `trace_id`, `retryable`, `errors[]` (com `field`, `message` e `reason`) e `existing`. Campo desconhecido é ignorado. Corpo vazio, HTML ou JSON inválido (413, 502, 504 da borda) vira um problema "da borda": `code` vazio, repetível se for `429` ou `5xx`. O `trace_id` vem do corpo ou do header `Dracma-Trace-Id`. **3. Paginação por cursor.** Segue `next_cursor` até ele ser nulo. O cursor é opaco: nunca o interprete. O parâmetro é `cursor` nas listagens de depósitos e saques e `after` em `GET /events`. Uma pausa de 200 ms entre páginas respeita o balde de listagem (5 req/s por chave). Há um limite de páginas contra laço infinito, que devolve o último cursor para retomar. **4. Poller de `GET /events`.** Uma rodada: lê o cursor guardado, busca os eventos seguintes, chama o **seu** aplicador e só então avança o cursor. Se o aplicador falhar, o cursor fica no evento anterior e o erro sobe. `400` com `reason: unknown` em `after` vira um erro próprio (cursor desconhecido) e o cursor **não** é zerado sozinho. O aplicador tem de ser o mesmo do webhook, com dedup pelo id do evento ([Recuperação com GET /events](../webhooks/recuperacao-get-events.md)). ### Exemplos #### Go ```go c, err := dracma.NewClientFromEnv() if err != nil { return err } corpo := []byte(`{"external_id":"dep_0192a3b4-5c6d-7e8f-9a0b-1c2d3e4f5a6b"}`) // o corpo completo vem do seu intento // 1. Repetição: mesma chave, mesmos bytes, nonce novo. resp, body, err := dracma.Retry(ctx, dracma.PoliticaRetry{}, func(ctx context.Context) (*http.Response, []byte, error) { return c.Do(ctx, "POST", "/deposits", corpo, "dep_0192a3b4-5c6d-7e8f-9a0b-1c2d3e4f5a6b") }) if errors.Is(err, dracma.ErrTentativasEsgotadas) { /* consulte por external_id */ } // 2. Erro legível. if resp.StatusCode >= 400 { p := dracma.ParseProblema(resp.StatusCode, resp.Header.Get("Content-Type"), resp.Header.Get("Dracma-Trace-Id"), body) if e, ok := p.Campo("player.email"); ok { /* mostre e.Reason no formulário */ } } // 3. Paginação. ultimo, err := dracma.Percorrer(ctx, c, dracma.Listagem{Caminho: "/deposits", Limite: 100, Intervalo: 200 * time.Millisecond}, func(item json.RawMessage) error { return aplicar(item) }) // 4. Poller (uma rodada a cada 5 minutos). n, err := dracma.RodarPoller(ctx, c, seuArmazem, dracma.OpcoesPoller{}, func(e dracma.Evento) error { return aplicador(e) }) ``` #### Node ```ts import { chamar, configDoAmbiente } from "./dracma.ts"; import { retry } from "./retry.ts"; import { parseProblema } from "./problema.ts"; import { percorrer } from "./paginacao.ts"; import { rodarPoller } from "./eventos.ts"; const cfg = configDoAmbiente(); const corpo = JSON.stringify({ external_id: "dep_0192a3b4-5c6d-7e8f-9a0b-1c2d3e4f5a6b" /* ... */ }); const r = await retry({}, () => chamar(cfg, "POST", "/deposits", corpo, "dep_0192a3b4-5c6d-7e8f-9a0b-1c2d3e4f5a6b")); if (r.status >= 400) { const p = parseProblema(r.status, r.headers.get("content-type"), r.headers.get("dracma-trace-id"), r.corpo); } for await (const item of percorrer(cfg, { caminho: "/deposits", limite: 100, intervaloMs: 200 })) { /* ... */ } const aplicados = await rodarPoller(cfg, seuArmazem, async (evento) => { /* o mesmo aplicador do webhook */ }); ``` #### PHP ```php require __DIR__ . '/Paginacao.php'; require __DIR__ . '/Http.php'; $cfg = Dracma::configDoAmbiente(); $r = Retry::executar([], static fn() => DracmaHttp::chamar($cfg, 'POST', '/deposits', $corpo, $chave)); if ($r['status'] >= 400) { $p = Problema::ler($r['status'], $r['headers']['content-type'] ?? null, $r['headers']['dracma-trace-id'] ?? null, $r['body']); } $get = static fn(string $alvo) => DracmaHttp::chamar($cfg, 'GET', $alvo); foreach (Paginacao::percorrer($get, '/deposits', ['limite' => 100, 'intervaloMs' => 200]) as $item) { /* ... */ } $n = Paginacao::rodarPoller($get, $seuArmazem, static function (array $evento): void { /* o mesmo aplicador do webhook */ }); ``` #### Shell (só para depurar) ```bash source exemplos/shell/helpers.sh export DRACMA_KEY_ID=key_test_SEU_ID DRACMA_KEY_SECRET_FILE=/caminho/seguro/segredo # Repetição: mesmos argumentos do assinar.sh; sai 0, 1 (4xx definitivo) ou 75 (esgotou) dracma_com_retry POST /deposits corpo.json "dep_0192a3b4-5c6d-7e8f-9a0b-1c2d3e4f5a6b" # Uma linha de JSON por item, seguindo next_cursor dracma_paginar /deposits cursor status=paid limit=100 # Uma rodada do poller; o cursor fica no arquivo dracma_poller ./cursor-eventos.txt ./aplicar-evento.sh # CPF de teste válido bash exemplos/shell/cpf-teste.sh 3 ``` #### Outros exemplos de uso ```bash # Go: ping assinado cd go && DRACMA_KEY_ID=key_test_SEU_ID DRACMA_KEY_SECRET_FILE=/etc/seu-servico/dracma.key go run ./cmd/ping # Shell: ver os headers que seriam enviados, sem chamar a rede SO_HEADERS=1 DRACMA_KEY_ID=key_test_SEU_ID DRACMA_KEY_SECRET_FILE=/etc/seu-servico/dracma.key \ bash shell/assinar.sh POST /deposits corpo.json dep_0192a3b4-5c6d-7e8f-9a0b-1c2d3e4f5a6b # Shell: conferir um webhook capturado DRACMA_WEBHOOK_SECRETS_FILE=/etc/seu-servico/dracma-webhook.keys \ bash shell/verificar-webhook.sh corpo.json "$TS" "$SIG" ``` #### Uma chamada por endpoint (curl assinado) Com `shell/assinar.sh`, que usa `curl` por baixo, cada endpoint é uma linha. Os corpos completos estão em [Depósitos](../referencia/depositos.md) e [Saques](../referencia/saques.md). | Endpoint | Comando | |---|---| | `GET /v1/ping` | `bash shell/assinar.sh GET /ping` | | `GET /v1/currencies` | `bash shell/assinar.sh GET /currencies` | | `POST /v1/deposits` | `bash shell/assinar.sh POST /deposits corpo.json "$EXTERNAL_ID"` | | `GET /v1/deposits/{id}` | `bash shell/assinar.sh GET "/deposits/$DEP_ID"` | | `GET /v1/deposits` | `bash shell/assinar.sh GET "/deposits?external_id=$EXTERNAL_ID"` | | `POST /v1/withdrawals` | `bash shell/assinar.sh POST /withdrawals corpo.json "$EXTERNAL_ID"` | | `GET /v1/events` | `bash shell/assinar.sh GET "/events?object_id=$DEP_ID"` | | `GET /v1/reconciliation` | `bash shell/assinar.sh GET "/reconciliation?date=2026-10-17¤cy=BRL"` | | `GET /v1/rules`, `GET /v1/balances` | `bash shell/assinar.sh GET /rules` e `bash shell/assinar.sh GET /balances` | ### Erros e casos de borda !!! warning "Não disponível" O Dracma não publica um SDK, nem clientes em Python, Java, C# ou Ruby, nem coleção de Postman ou Bruno. Os exemplos acima não incluem uma função tipada de criação de depósito ou saque: os tipos dos corpos estão em [Objetos e campos](../referencia/schemas.md) e no `openapi.yaml`. Para outra linguagem, porte a lógica destes arquivos e confira com os vetores. - **Importar o `openapi.yaml`.** O arquivo publicado em [OpenAPI](../referencia/openapi.md) pode ser importado no Postman ou no Bruno para montar uma coleção, e alimenta o `openapi-generator` para gerar tipos e rotas. O gerador **não** conhece a assinatura HMAC: você precisa acrescentar o cabeçalho `Dracma-Signature` (e os outros três) num interceptor que assine os bytes exatos do corpo, e a verificação de webhook continua sendo sua. Valide o resultado com os vetores. Antes de confiar no cliente gerado, leia as divergências conhecidas em [Divergências conhecidas](../referencia/divergencias.md). - **PHP.** `Dracma::chamar()` do arquivo principal não devolve os headers da resposta, e o `Retry-After` é um deles: use `DracmaHttp::chamar()` com os helpers. - **Node abaixo de 22.18.** O TypeScript direto não roda. Use `tsx` ou compile; a lógica é a mesma. - **Shell.** `helpers.sh` precisa de `jq` ou `python3` para ler JSON. Os temporários ficam numa pasta oculta ao lado do script e são apagados no fim. ### Veja também - [Autenticação](../api/autenticacao.md) e [Idempotência e repetição](../api/idempotencia.md). - [Paginação e filtros](../api/paginacao.md) e [Erros](../api/erros.md). - [Recuperação com GET /events](../webhooks/recuperacao-get-events.md). - [Dados de teste](../sandbox/dados-de-teste.md): `cpf-teste.sh`. - [Arquitetura do conector](../conector/arquitetura.md). --- # Arquivo: docs/casos/README.md ## Casos > Para: Adm — Conferido em 2026-10-18. Casos reais de integração, para você ver como as páginas gerais viram decisões num produto. Um caso não é regra: o que vale para todos os operadores está nas partes Conceitos, API, Dinheiro e Webhooks. ### O que é Esta seção tem duas coisas: | Página | O que traz | |---|---| | [Ficha do operador](ficha-do-operador.md) | Um modelo de ficha para descrever a sua operação: moeda, PSP, regras, horários, webhook, equipe, contatos. Preencha uma por operação | | [Caso Nero](nero/README.md) | O conector de uma casa de apostas, do front ao livro, com as decisões que ela tomou. É o exemplo completo de como aplicar o desenho | ### Quando usar - **Ficha do operador**: no onboarding, para combinar com o Dracma o perfil da operação; e depois, como documento vivo do seu lado, para o time novo entender a operação. - **Caso Nero**: quando você já leu as páginas gerais e quer ver as mesmas regras aplicadas ponta a ponta, com rotas de portal, estados do front, tabela de decisões e checklist de go-live com prova. ### Como funciona Cada caso segue a mesma estrutura e diz, no início, o que é geral e o que é decisão daquela empresa: - O que o caso **herda** das páginas gerais fica como link, sem repetir. - O que é **específico** (nomes de rotas, nomes de serviços, decisões do dono, números) fica no caso. - Onde o caso e o contrato divergem, vale o contrato e as páginas gerais. O caso é um exemplo, não uma exceção. !!! note "Reservado no contrato" Os nomes de serviços, rotas e métricas dos casos são do operador do caso. O Dracma não oferece essas rotas nem exige esses nomes. ### Exemplos Comece pela [Ficha do operador](ficha-do-operador.md) e depois pelo [Caso Nero](nero/README.md), na ordem de leitura que ele propõe. ### Erros e casos de borda - Não copie valores de um caso para a sua operação sem conferir com `GET /v1/rules`, `GET /v1/currencies` e o seu contrato com o Dracma: multiplicador, horários e PSP mudam por operação. - Decisões de dono de um caso (por exemplo, o que fazer com um estorno) são decisões da empresa do caso. A sua pode ser diferente, mas a base segura é a da [Política de exceções de dinheiro](../conector/politica-de-excecoes.md). ### Veja também - [Arquitetura do conector](../conector/arquitetura.md). - [Checklist de go-live](../go-live/checklist.md). - [Início](../README.md). --- # Arquivo: docs/casos/ficha-do-operador.md ## Ficha do operador > Para: Adm — Conferido em 2026-10-18. Um modelo de ficha para descrever uma operação em uma página: o perfil que você combina com o Dracma no onboarding e que o seu time consulta depois. Uma ficha por operação. ### O que é A ficha é um documento **seu**, versionado junto do conector. Ela não substitui o contrato: reúne, no mesmo lugar, o que está espalhado entre o painel, a API e o que foi combinado no onboarding. A coluna "Onde obter" diz de onde sai cada valor, para você preencher sem adivinhar. ### Quando usar - No onboarding, ao pedir a operação Teste e, de novo, ao pedir a operação Real. - Quando algo muda: novo PSP, nova regra de saque, novo webhook, pessoa nova na equipe. - No go-live, anexada ao registro, como prova de que o perfil combinado é o que está configurado ([Checklist de go-live](../go-live/checklist.md)). ### Como funciona #### O modelo Copie a tabela e preencha a coluna "Valor". Os valores abaixo são fictícios. | Campo | Valor (exemplo) | Onde obter | |---|---|---| | Nome da operação | `operador-br-teste` | Painel, seletor de operação ([O painel](../painel/README.md)) | | Tipo | Teste (ou Real) | `GET /v1/ping`: `environment` `sandbox` é Teste; Real é `production` | | Id da operação | `00000000-0000-0000-0000-000000000001` | `GET /v1/ping`: `operation_id` | | Host da API | Informado no onboarding | [Ambientes, tipos de operação e hosts](../conceitos/ambientes-e-hosts.md) | | Moeda e método | BRL, `pix` (depósito e saque) | `GET /v1/currencies` ([Ping e moedas](../referencia/ping-e-moedas.md)) | | Exigências de depósito | `player.name` | `GET /v1/currencies`: `deposit_requirements` | | PSP de depósito | Informado pelo Dracma | Onboarding. É escolhido por operação e não aparece na API ([PSPs](../dinheiro/psps-e-ambientes.md)) | | PSP de saque | Informado pelo Dracma | Onboarding | | Multiplicador do saque automático (X×) | Ex.: 1 | `GET /v1/rules` ([Regras](../referencia/regras.md)) | | Janela e horário do saque automático | Ex.: 24x7 | `GET /v1/rules` e [Dia, fuso e horários](../dinheiro/fuso-dia-e-horarios.md) | | Fuso da operação | Ex.: America/Sao_Paulo | Onboarding | | Primeiro saque do jogador | Vai para revisão? | `decision.reasons` com `FIRST_WITHDRAWAL` | | Jogadores antigos | Contam ou não para o multiplicador? | Decisão sua; veja o parágrafo abaixo | | Tarifas | Por contrato | Contrato com o Dracma ([Tarifas](../dinheiro/tarifas.md)) | | Caixa inicial e alerta de caixa baixo | Valor e limiar | `GET /v1/balances` e [Saldos e caixa livre](../dinheiro/saldos.md) | | URL do webhook | `https://pagamentos.operador.example/dracma/webhook` | Painel ([Webhooks no painel](../painel/webhooks.md)) | | Eventos assinados | Todos, ou lista | Painel | | Contatos de alerta | Até 5 e-mails | Painel ([Alertas e e-mails](../painel/alertas.md)) | | Chaves ativas e escopos | Até 2 ativas | Painel ([Chaves de API](../painel/chaves-de-api.md)) | | Equipe e papéis | Quem aprova, quem revisa, quem vê | [Equipe e papéis](../painel/equipe-e-papeis.md) | | Quem decide as exceções de dinheiro | Cargo e substituto | [Política de exceções de dinheiro](../conector/politica-de-excecoes.md) | | Conector | Nome do serviço, repositório e responsável | Sua empresa | | Jobs | Poller de eventos (5 min), reconciliação (hora), relógio | [Arquitetura do conector](../conector/arquitetura.md) | Sobre jogadores antigos: o multiplicador conta só o que o Dracma liquidou. Quem já tinha depositado antes da operação existir não tem histórico no Dracma e, conforme as regras da operação, o primeiro saque pode ir para revisão (`FIRST_WITHDRAWAL` ou `NO_SETTLED_DEPOSIT`). Anote aqui a sua escolha (deixar assim, ou revisar manualmente os primeiros saques) para o time de suporte saber o que esperar. #### Como manter a ficha 1. Guarde a ficha no repositório do conector, em Markdown. 2. Mude-a no mesmo pedido de mudança que altera a configuração. 3. No go-live, compare a ficha com o painel e com `GET /v1/rules` e `GET /v1/currencies`: qualquer diferença é um bug de um dos dois lados. ### Exemplos Um preenchimento real, de uma casa de apostas, está no [Caso Nero](nero/README.md), seção "A Nero no Dracma". ### Erros e casos de borda - Campo que depende do Dracma e não aparece na API (PSP, tarifa, fuso): peça ao suporte e anote a data da resposta. - Operação Teste e Real têm fichas diferentes: o PSP, os limites e a equipe podem não ser os mesmos. - Não coloque segredo nem chave na ficha. Ela guarda **onde** estão, nunca o valor. ### Veja também - [Casos](README.md). - [Pré-requisitos do onboarding](../onboarding/pre-requisitos.md). - [De Teste para Real](../onboarding/promocao-para-producao.md). --- # Arquivo: docs/casos/nero/README.md ## Guia da Nero: o conector `nero_payments` > Para: Dev, Adm — Conferido em 2026-10-18. Um caso real: o conector `nero_payments` de uma casa de apostas, único serviço dela que fala com o Dracma. A parte do Dracma foi conferida na data acima. A parte da Nero (nomes de serviços, rotas do portal e decisões do dono) é a descrição adotada pelo time dela. ### O que é A Nero é uma bet que usa o Dracma para depósito e saque por PIX. Este caso reúne o que é **específico dela**: as rotas do portal que o front consome, o mapeamento de estados para a tela do jogador, o outbox, a fila `needs_decision` e as decisões do dono (D1 a D24). Tudo que vale para qualquer operador está nas páginas gerais e aparece aqui só como link. #### A Nero no Dracma | Item | Valor | |---|---| | Tenant | A pessoa jurídica da Nero | | Operações | `nero-br-hml` (operação Teste, chave `key_test_`) e `nero-br-prd` (operação Real, chave `key_live_`) | | Conector | `nero_payments`, único dono da credencial do Dracma | | Moeda e método | BRL e `pix`, em depósito e saque | | PSP de depósito | Escolhido pelo Dracma por operação. Com o Mercado Pago, `player.name` e `player.email` são exigidos; com a MaximaxPay, `player.name` | | PSP de saque | MaximaxPay | | Multiplicador do saque automático | X = 1 (D2): o jogador saca no automático até o que depositou e o Dracma liquidou | | Jogadores antigos | Depósitos feitos antes do Dracma **não** contam para o X× (D3): o primeiro saque de um jogador antigo vai para revisão | A tabela completa de PSPs por operação está em [PSPs](../../dinheiro/psps-e-ambientes.md). O modelo de ficha para descrever uma operação está em [Ficha do operador](../ficha-do-operador.md). #### Decisões do dono que moldam o conector | Decisão | Efeito no conector | |---|---| | D1 | Tratamento dos saques de jogadores antigos (decisão registrada pelo dono; ver o item N33 do [Go-live da Nero](go-live-nero.md)) | | D2 | X = 1 | | D3 | Depósitos anteriores ao Dracma não contam para o X× | | D4 | Pagamento de terceiro ou de valor diferente: **devolver** | | D5 | Em produção sem credencial do Dracma, depósito e saque respondem `503 payment_provider_not_configured` e o resto da bet funciona | | D6 | Depósito creditado que vira `refunded` ou `chargeback`: não debita; `needs_decision` | | D7 | Saque `paid` que vira `returned`: não recredita; `needs_decision` | | D8 | Depósito pago depois de expirar ou cancelar: não credita; `needs_decision` | | D11 | Ações e permissões do backoffice sobre `needs_decision` | | D12 | `409` no envio de saque: reserva mantida, nada devolvido nem reenviado | | D18 | Saque `recalled`: como D7 | | D24 | Nunca mostrar ao jogador o estado bancário nem a palavra "compliance" | ### Quando usar Leia este caso depois das páginas gerais, quando quiser ver o desenho completo aplicado: portal, conector, livro e backoffice. Para outro produto, copie a estrutura e troque as decisões pelas suas. #### Ordem de leitura | # | Página | O que você sai sabendo | |---|---|---| | 1 | [Fluxo ponta a ponta](fluxo.md) | O caminho jogador, portal, conector, Dracma, PSP, webhook, fetch-back e carteira | | 2 | [Estados no front](estados-no-front.md) | Como cada estado do Dracma vira um estado do front | | 3 | [Depósito](deposito.md) | Como implementar a rota de depósito do portal | | 4 | [Saque](saque.md) | Como implementar a rota de saque: reserva antes, `player.ip`, chave PIX | | 5 | [Webhook, eventos e reconciliação](webhook-e-jobs.md) | Rota do webhook, job de `GET /events`, reconciliação, métricas e alertas | | 6 | [Revisão e decisões](revisao-e-decisoes.md) | `under_review` e a fila `needs_decision` | | 7 | [Rede e segredos](rede-e-segredos.md) | Credenciais, egress restrito e rotação | | 8 | [Primeiros passos no sandbox](primeiros-passos-sandbox.md) | O primeiro depósito e o primeiro saque, com as constantes da Nero | | 9 | [Go-live da Nero](go-live-nero.md) | Checklist de produção com a prova de cada item | Referência geral: [Autenticação](../../api/autenticacao.md), [Webhooks](../../webhooks/README.md), [Depósitos](../../referencia/depositos.md), [Saques](../../referencia/saques.md), [Idempotência](../../api/idempotencia.md), [Reconciliação](../../referencia/reconciliacao.md), [Sandbox](../../sandbox/README.md) e [Exemplos executáveis](../../exemplos/README.md). ### Como funciona #### As regras que não se negociam 1. **Credita só em `paid` lido por fetch-back** (`GET /v1/deposits/{id}` com a credencial da Nero), nunca pelo corpo do webhook nem pelo front. 2. **Reserva antes do saque**, na mesma transação que grava o `dracma_intents`. A reserva só volta em `rejected`, `failed` ou `cancelled` confirmados, ou em `4xx` conclusivo. 3. **`Idempotency-Key` igual ao `external_id`** (`dep_` ou `wdr_`), gravados **antes** da chamada. Repetição de `POST` de dinheiro usa **sempre a mesma chave**. 4. **Estado desconhecido não move dinheiro.** Nem crédito, nem débito, nem devolução de reserva. 5. **Nada de automático em débito.** Estorno, chargeback, `returned`, `recalled` e pagamento depois de expirar vão para `needs_decision` e um humano decide (D6, D7, D8, D18). 6. **A identidade do jogador vem do portal** (`X-Nero-Identity`), nunca do corpo. `player.external_ref` é o id interno imutável do jogador, nunca e-mail, telefone ou CPF. 7. **Segredo só no `nero_payments`**: arquivo `0400` do usuário Linux do serviço, entregue pelo ops da Nero. Nunca em front, bundle, log, repositório ou variável `NEXT_PUBLIC_*`. A lista completa no formato MUST e NEVER está na [Referência rápida](../../referencia-rapida.md). ### Exemplos Os trechos de código e os corpos de requisição estão nas páginas de [Depósito](deposito.md) e [Saque](saque.md). O caminho completo em comandos está em [Primeiros passos no sandbox](primeiros-passos-sandbox.md). ### Erros e casos de borda #### Pontos a confirmar do lado da Nero | Ponto | Por quê | Sugestão | |---|---|---| | Formato de `amount` nas **respostas** do portal | O contrato do portal fixa o pedido (número em reais), não a resposta | Devolver no mesmo formato do pedido, gerado do inteiro por formatação exata, sem float | | Valores legados de `pix_type` no corpo do saque | O front antigo pode mandar `evp`, `aleatoria` ou `telefone` | Mapear para `random` e `phone` ([Saque](saque.md)) | | Exibir `expected_settlement_date` e estados bancários (D24) | Só existem em transferência bancária | Manter `processing` e `under_review` até a Nero decidir | !!! note "Reservado no contrato" Os campos de transferência bancária (`expected_settlement_date`, `rail`, `tracking`) existem no contrato mas ficam nulos em PIX. Veja [Rotas e campos reservados](../../referencia/superficies-reservadas.md). ### Veja também - [Casos](../README.md) e [Ficha do operador](../ficha-do-operador.md). - [Arquitetura do conector](../../conector/arquitetura.md): o desenho geral que este caso aplica. - [Estados no seu produto](../../conector/estados-no-produto.md) e [Política de exceções de dinheiro](../../conector/politica-de-excecoes.md). --- # Arquivo: docs/casos/nero/fluxo.md ## Fluxo ponta a ponta > Para: Dev — Conferido em 2026-10-18. Quem fala com quem, em que ordem, e onde cada garantia (idempotência, fetch-back, reserva) acontece no conector da Nero. Os detalhes de implementação estão em [Depósito](deposito.md), [Saque](saque.md) e [Webhook, eventos e reconciliação](webhook-e-jobs.md). ### O que é O desenho geral está em [Arquitetura do conector](../../conector/arquitetura.md). Aqui ficam os nomes dos componentes da Nero e as fronteiras entre eles. ```mermaid flowchart LR J["Jogador: navegador ou app"] -->|"sessão da bet, cookie HttpOnly"| F["nero_client (front)"] F -->|"HTTPS, sem segredo no bundle"| P["nero_portal"] P -->|"X-Nero-Identity (asserção assinada)"| C["nero_payments (conector e outbox dracma_intents)"] C <-->|"reserva, crédito, devolução"| L["nero_ledger (carteira)"] C -->|"HMAC e Idempotency-Key"| D["Dracma API /v1"] D -->|"credencial de PSP, nunca sai do Dracma"| PSP["PSP PIX"] PSP -->|"callback, termina no Dracma"| D D -->|"webhook assinado"| CD["Caddy da bet"] CD -->|"POST /api/payments/dracma/webhook"| C C -->|"fetch-back em /v1/deposits/id e /v1/withdrawals/id"| D ``` | Fronteira | O que cruza | O que **nunca** cruza | |---|---|---| | Jogador para o portal | Sessão, valor, método, chave PIX de saque, CPF | Nada do Dracma | | Portal para o `nero_payments` | `X-Nero-Identity` (quem é o jogador) e o corpo do pedido | Identidade vinda do corpo | | `nero_payments` para o Dracma | `external_id`, `player.external_ref` (opaco), documento, nome, e-mail, IP e device (risco e exigências), valor na menor unidade, destino do saque, `review_requested` | Senha, sessão, token da bet, dados de jogo | | Dracma para o `nero_payments` | Objetos de depósito e saque, BR Code, eventos assinados, reconciliação | Credencial de PSP, dados de outra operação | ### Quando usar Use esta página para ver o conjunto antes de implementar uma rota. Os dois diagramas abaixo mostram o caminho feliz; os casos de borda estão em cada página de implementação. ### Como funciona #### Depósito ```mermaid sequenceDiagram autonumber participant J as Jogador (front) participant P as nero_portal participant C as nero_payments participant O as outbox dracma_intents participant D as Dracma participant S as PSP PIX J->>P: POST /api/payments/deposits {amount: 50.00, method: pix} P->>C: repassa com X-Nero-Identity C->>O: grava intent (external_id dep_uuidv7, valor em centavos, player_ref) na MESMA transação C->>D: POST /v1/deposits (Idempotency-Key = external_id, timeout 10 s) D->>S: cria a cobrança PIX D-->>C: 201 pending com instructions.pix (ou 202 created) C-->>J: 201 {deposit_id, status: pending, pix.copy_paste, expires_at} J->>S: paga o PIX no app do banco S-->>D: confirmação do pagamento D->>D: confere valor e pagador e vai a paid D->>C: webhook deposit.paid (assinado) C->>C: verifica assinatura e janela, dedup por Dracma-Event-Id C->>D: GET /v1/deposits/dep_... (fetch-back) D-->>C: status paid, amount, external_id, player_ref C->>C: confere com o intent e credita UMA vez (idempotente por external_id) C-->>D: 200 J->>P: GET /api/payments/deposits/dep_... (polling) P->>C: repassa C-->>J: status paid ``` - Se o `POST /v1/deposits` der timeout, `5xx` ou `429`, o intent continua no outbox e um worker repete com a **mesma** chave. O front recebe `202 {status: creating}` e faz polling. - Se o webhook se perder, o job de `GET /v1/events` (a cada 5 minutos) encontra o `deposit.paid` e passa pelo mesmo caminho: dedup, fetch-back e crédito. - O polling do front lê o **estado local** do `nero_payments`. Não é um proxy para o Dracma. #### Saque ```mermaid sequenceDiagram autonumber participant J as Jogador (front) participant P as nero_portal participant C as nero_payments participant L as nero_ledger participant D as Dracma participant S as PSP PIX J->>P: POST /api/wallet/withdraw/request {amount, type: pix, pix_key, pix_type} P->>C: repassa com X-Nero-Identity e o IP do jogador C->>L: RESERVA o valor (débito) na mesma transação do intent wdr_uuidv7 C->>D: POST /v1/withdrawals (Idempotency-Key = external_id, player.ip) D->>D: motor de regras: automático, revisão ou bloqueio alt bloqueio (422) D-->>C: 422 com o code C->>L: devolve a reserva C-->>J: 200 {status: false, message} (mensagem genérica) else automático ou revisão D-->>C: 201 approved ou under_review C-->>J: 200 {status: true, withdrawal_id, state: processing ou under_review} end D->>S: envia o PIX (automático, ou depois da aprovação no painel) S-->>D: liquidado (E2E id) D->>C: webhook withdrawal.paid C->>D: GET /v1/withdrawals/wdr_... (fetch-back) D-->>C: status paid, psp_reference C->>L: finaliza a reserva (saída definitiva) C-->>D: 200 ``` - **A reserva vem antes do `POST`** e só volta em `rejected`, `failed` ou `cancelled` (confirmados por fetch-back) ou em `4xx` conclusivo. Timeout e `5xx` **não** devolvem reserva. - `under_review` é decidido por revisores da Nero **no painel do Dracma**. O resultado chega por webhook como `approved`, `processing`, `paid`, ou `rejected`. #### O outbox `dracma_intents` O modelo geral (colunas, estados e regras) está em [Arquitetura do conector](../../conector/arquitetura.md). Na Nero: - A tabela se chama `dracma_intents`. O `external_id` é `dep_` ou `wdr_` e é também o `deposit_id` e o `withdrawal_id` que o front enxerga. - Backoff de 1 s, 2 s, 4 s e assim por diante, até 60 s, respeitando o `Retry-After`. - A métrica `nero_dracma_outbox_pending` e o alerta de intent pendente há mais de 10 minutos estão em [Webhook, eventos e reconciliação](webhook-e-jobs.md). ### Exemplos O passo a passo em comandos, contra a operação `nero-br-hml`, está em [Primeiros passos no sandbox](primeiros-passos-sandbox.md). ### Erros e casos de borda #### O que o Dracma garante e o que é da Nero | Garantia | Quem | |---|---| | Não criar duas cobranças nem dois saques para a mesma chave | Dracma, **desde que a Nero repita com a mesma chave** | | Não pagar para chave PIX de terceiro; decidir automático, revisão ou bloqueio | Dracma (motor de regras e consulta de titularidade) | | Creditar uma vez, só em `paid` | Nero (fetch-back e idempotência por `external_id`) | | Reservar antes, devolver só em estado conclusivo | Nero | | Não perder evento | Os dois: o Dracma reentrega por 72 horas e mantém `GET /events`; a Nero roda o job de 5 minutos | | Provar que os dois livros batem | Os dois: reconciliação diária com digest assinado | ### Veja também - [Estados no front](estados-no-front.md). - [Idempotência e repetição](../../api/idempotencia.md). - [Receptor passo a passo](../../webhooks/receptor.md). --- # Arquivo: docs/casos/nero/estados-no-front.md ## Estados no front da Nero > Para: Dev — Conferido em 2026-10-18. O front da Nero (`nero_client`) não conhece o Dracma. Ele vê um conjunto fechado de estados, fixado no contrato do portal, que o `nero_payments` calcula a partir de duas fontes: o estado local do outbox e o último estado do Dracma **aplicado** (lido por fetch-back). ### O que é Esta página é a tabela de tradução entre os estados do Dracma e os do front. A regra geral é que o estado mostrado ao jogador **nunca adianta dinheiro**: `paid` só aparece depois que o crédito (depósito) ou a finalização (saque) foi gravado no `nero_ledger`. A versão genérica desta tabela, para qualquer produto, está em [Estados no seu produto](../../conector/estados-no-produto.md). #### Rotas do portal | Método e rota (portal) | Autenticação | Pedido e resposta | |---|---|---| | `POST /api/payments/deposits` | Jogador | `{amount: number (reais, ex. 50.00), method: "pix", accept_bonus: bool, cpf?: string}` devolve `201` ou `202` com `{deposit_id, status, amount, currency, method, pix?: {copy_paste, qr_code_png_base64?, expires_at}, expires_at}` | | `GET /api/payments/deposits/{deposit_id}` | Jogador, só o dono (outro recebe `404`) | Mesmo formato; o front faz polling até `paid` ou `expired` | | `POST /api/wallet/withdraw/request` | Jogador | Corpo legado `{amount, type: "pix", pix_key, pix_type, accept_terms, currency?}` devolve `200 {status: true, withdrawal_id, state: "processing" ou "under_review", message}`; os erros de negócio mantêm as mensagens atuais | | `GET /api/payments/withdrawals/{withdrawal_id}` | Jogador, só o dono | `{withdrawal_id, state, amount, currency, created_at, updated_at}` | | `POST /api/payments/dracma/webhook` | Nenhuma no portal; assinatura no conector | `200`, `401`, `400` ou `503` ([Webhook, eventos e reconciliação](webhook-e-jobs.md)) | `deposit_id` e `withdrawal_id` **são o `external_id`** enviado ao Dracma (`dep_`, `wdr_`), opacos para o front. O id do Dracma fica só no `nero_payments`. Rotas legadas removidas (respondem `404` depois do corte): `/api/wallet/deposit/payment`, `/api/payments/{deposits,callbacks,status}/{gateway}`, `/api/{gateway}/qrcode-pix`, `/api/{gateway}/consult-status-transaction` e os callbacks dos gateways antigos. ### Quando usar Ao implementar a tradução de estado no `nero_payments` e a tela de depósito e saque. Confira cada linha com o time de produto antes de fixar as mensagens. ### Como funciona #### Depósito: Dracma para o front Estados do front: `creating`, `pending`, `paid`, `expired`, `cancelled`, `failed`, `under_review`. | Situação no `nero_payments` | Estado do Dracma (fetch-back) | Front vê | Dinheiro na carteira | Mensagem sugerida | |---|---|---|---|---| | Intent gravado, sem resposta conclusiva (timeout, `5xx`, `429`) | Nenhum | `creating` | Nada | "Gerando seu PIX..." | | `202` do Dracma | `created` | `creating` | Nada | "Gerando seu PIX..." | | Nenhuma | `pending` | `pending` (com `pix`) | Nada | "Pague o PIX até {expires_at}" | | Nenhuma | `under_review` | `under_review` | Nada | "Pagamento recebido, em análise." | | Crédito gravado no ledger | `paid` | `paid` | **Creditado uma vez** | "Depósito confirmado." | | Nenhuma | `expired` | `expired` | Nada | "O PIX expirou. Gere outro." | | Nenhuma | `cancelled` | `cancelled` | Nada | "Depósito cancelado." | | Nenhuma | `failed` | `failed` | Nada | "Não foi possível gerar o PIX. Tente de novo." | | Nenhuma | `returned` | `failed` | Nada | "O pagamento não pôde ser aceito. Fale com o suporte." | | `paid` já creditado, depois estorno ou chargeback | `refunded` ou `chargeback` | `paid` (continua) | **Não mexe**: `needs_decision` (D6) | Tratado pelo backoffice | | Nenhuma | `pending_compliance` ou `frozen` | `under_review` | Nada | "Pagamento em análise." | | Nenhuma | `paid_partial` | `under_review` | Nada: `needs_decision` | "Pagamento em análise." | | Nenhuma | Estado desconhecido | Mantém o último estado mostrado (ou `under_review` se era `pending`) | Nada | "Pagamento em análise." | | `4xx` na criação (sem objeto no Dracma) | Nenhum | Erro HTTP ao front (sem `deposit_id` utilizável) | Nada | Tabela de erros em [Depósito](deposito.md) | | Produção sem Dracma configurado (D5) | Nenhum | Erro `503 payment_provider_not_configured` | Nada | "Depósito indisponível no momento." | - `returned` não existe na lista de estados do front: a Nero mostra `failed` com a mensagem de devolução. - `refunded` e `chargeback` acontecem **depois** de o jogador ter visto `paid`. A tela não volta atrás; o caso vai para a fila `needs_decision` ([Revisão e decisões](revisao-e-decisoes.md#needs_decision-quando-abrir)). - Nunca explique `under_review` com o motivo (`PAYER_MISMATCH`, `AMOUNT_MISMATCH`) ao jogador; o suporte vê. - Com o Mercado Pago, `under_review` por `PAYER_UNKNOWN` fica mais comum, porque o PSP não informa o pagador ([PSPs](../../dinheiro/psps-e-ambientes.md)). !!! note "Reservado no contrato" `pending_compliance`, `frozen` e `paid_partial` fazem parte do contrato, mas a operação PIX não os produz hoje. O conector da Nero os trata como `under_review` por segurança. #### Saque: Dracma para o front Estados do front: `processing`, `under_review`, `paid`, `rejected`, `failed`, `cancelled`. | Situação no `nero_payments` | Estado do Dracma (fetch-back) | Front vê | Reserva no ledger | Mensagem sugerida | |---|---|---|---|---| | Intent e reserva gravados, sem resposta conclusiva | Nenhum | `processing` | **Mantida** | "Saque em processamento." | | Nenhuma | `requested` (consulta de titularidade pendente) | `processing` | Mantida | "Saque em processamento." | | Nenhuma | `approved` ou `processing` | `processing` | Mantida | "Saque em processamento." | | Nenhuma | `under_review` | `under_review` | Mantida | "Saque em análise." | | Finalização gravada | `paid` | `paid` | **Finalizada** (saída definitiva) | "Saque enviado." | | Nenhuma | `rejected` | `rejected` | **Devolvida** | "Saque não aprovado. O valor voltou para sua carteira." | | Nenhuma | `failed` | `failed` | **Devolvida** | "Não foi possível enviar o PIX. O valor voltou para sua carteira." | | Nenhuma | `cancelled` | `cancelled` | **Devolvida** | "Saque cancelado. O valor voltou para sua carteira." | | `paid` já finalizado, o banco devolveu | `returned` | `under_review` | **Não mexe**: `needs_decision` (D7) | "Saque em análise." | | Nenhuma | `pending_compliance`, `sent`, `in_transit` ou `recall_requested` | `processing` | Mantida | "Saque em processamento." (nunca dizer "compliance", D24) | | Nenhuma | `recalled` | `under_review` | Não mexe: `needs_decision` (D18) | "Saque em análise." | | Nenhuma | Estado desconhecido | `processing` | Mantida | "Saque em processamento." | | `4xx` na criação (`422`, `400`) | Nenhum | `200 {status: false, message}` (mensagem legada) | **Devolvida** | [Saque](saque.md) | | `409 idempotency_key_reused` no envio | Nenhum | `processing` | Mantida: `needs_decision` (D12) | "Saque em processamento." | Resposta do `POST /api/wallet/withdraw/request`: `state: "under_review"` quando o `201` do Dracma veio com `under_review`; `state: "processing"` em todos os outros casos aceitos, inclusive quando o Dracma não respondeu de forma conclusiva (o pedido está no outbox e a reserva está feita). O `returned` do saque aparece como `under_review` no front e abre `needs_decision`: a Nero não recredita sozinha (D7). O dinheiro volta ao caixa da operação no Dracma, e o backoffice decide se recredita a carteira. #### Como calcular o estado sem corrida ```text estado_front(intent): se intent.dracma_version é nulo: # o Dracma ainda não respondeu de forma conclusiva devolve "creating" (depósito) ou "processing" (saque) s = intent.dracma_status # último estado APLICADO (fetch-back, versão maior) devolve TABELA[s] ou PADRAO_DESCONHECIDO ``` - Aplique um estado novo só se a `version` for **maior** que `intent.dracma_version` (webhooks chegam fora de ordem). - O efeito de dinheiro e a atualização de `dracma_status` vão na **mesma transação** do ledger. Assim o front nunca vê `paid` antes do crédito. - O polling do front **não** chama o Dracma a cada pedido. Se o intent está parado em `creating` ou `pending` há mais de 60 segundos, o `nero_payments` pode fazer **um** fetch-back por id (no máximo a cada 10 segundos por objeto), além do webhook e do job de `GET /events`. ### Exemplos A tabela acima é o exemplo. Para o caminho completo com comandos, veja [Primeiros passos no sandbox](primeiros-passos-sandbox.md). ### Erros e casos de borda - Estado do Dracma que a tabela não conhece: nunca mova dinheiro. Mantenha o último estado e registre. - Versão do evento menor ou igual à aplicada: ignore (já foi superada). - Divergência entre o estado local e o fetch-back: vale o fetch-back; registre a diferença. ### Veja também - [Estados no seu produto](../../conector/estados-no-produto.md). - [Ciclo do depósito](../../dinheiro/ciclo-do-deposito.md) e [Ciclo do saque](../../dinheiro/ciclo-do-saque.md). --- # Arquivo: docs/casos/nero/deposito.md ## Depósito no `nero_payments` > Para: Dev — Conferido em 2026-10-18. Como implementar `POST /api/payments/deposits` e `GET /api/payments/deposits/{deposit_id}` sobre o `POST /v1/deposits` do Dracma. A referência da API está em [Depósitos](../../referencia/depositos.md). ### O que é A rota do portal recebe o pedido do jogador em reais, converte para a menor unidade, grava o intent e chama o Dracma. Na Nero há três particularidades: a conversão do valor vindo do front, as exigências por operação (que mudam com o PSP) e o campo `accept_bonus`, que fica de fora da chamada. ### Quando usar Ao implementar ou revisar a rota de depósito do `nero_payments`. ### Como funciona #### Passo a passo do `POST /api/payments/deposits` ```mermaid flowchart TD A["Pedido do front com X-Nero-Identity"] --> B{"Corpo válido? amount em reais com até 2 casas, method = pix"} B -->|não| B1["400 ao front"] B -->|sim| C["Carrega o jogador pelo id da asserção: external_ref, CPF, nome, e-mail, IP"] C --> D{"Exigências da operação (GET /v1/currencies em cache) atendidas?"} D -->|não| D1["422 ao front: complete o cadastro, sem chamar o Dracma"] D -->|sim| E["Transação: grava intent dep_uuidv7 com os bytes do corpo"] E --> F["POST /v1/deposits com Idempotency-Key igual ao external_id, timeout 10 s"] F -->|"201 pending"| G["Aplica o estado e devolve 201 com o pix"] F -->|"202 created"| H["Devolve 202 com status creating"] F -->|"4xx"| I["Intent rejected e erro mapeado"] F -->|"timeout, 5xx, 429"| J["Intent segue pending no outbox e 202 creating"] ``` 1. **Identidade.** O jogador vem da asserção `X-Nero-Identity` do portal. Nada de `player_id`, e-mail ou CPF de outro jogador vindo do corpo. 2. **Valor sem float.** O front manda `amount` em reais (`50.00`). Converta para a menor unidade **por texto**, nunca por multiplicação de ponto flutuante ([Convenções HTTP](../../conceitos/convencoes-http.md#dinheiro-money)): | Entrada (`amount`) | Menor unidade (`amount.amount`) | |---|---| | `50` | `"5000"` | | `50.5` | `"5050"` | | `50.00` | `"5000"` | | `0.01` | `"1"` | | `50.001`, `-1`, `0`, `1e3`, `"50,00"` | **Recuse** com `400` | ```text centavos(numero_json_como_texto): exigir casa com ^[0-9]{1,12}(\.[0-9]{1,2})?$ # sem sinal, sem expoente, até 2 casas inteiro, frac = separar no "."; frac = (frac + "00")[0:2] c = remover_zeros_a_esquerda(inteiro + frac); exigir c != "0" devolver c # string de inteiro ``` Em Go, leia o corpo com `json.Decoder.UseNumber()`. Em Node, não use `JSON.parse` direto no valor: leia o número como texto ou use um parser que preserve o literal. 3. **Exigências da operação.** Antes de montar o corpo, confira `deposit_requirements` do `GET /v1/currencies` (em cache; recarregue no boot e a cada hora). Com o Mercado Pago como PSP de depósito, o Dracma exige `player.name` **e** `player.email`; com a MaximaxPay, `player.name`. Se o jogador não tem o campo, responda ao front pedindo para completar o cadastro, **sem** chamar o Dracma. 4. **Documento.** O `cpf` do corpo só é aceito se for o CPF do cadastro do jogador (ou o primeiro cadastro dele). O Dracma **fixa** o documento no primeiro uso: documento diferente depois dá `422 document_mismatch`. 5. **Outbox.** Na mesma transação, gere `external_id = dep_`, serialize o corpo **uma vez** e grave os bytes. A `Idempotency-Key` é o próprio `external_id`. 6. **Chamada.** `POST /v1/deposits` assinado, timeout de 10 segundos. Trate a resposta pela tabela abaixo. #### Corpo enviado ao Dracma ```json { "external_id": "dep_0192a3b4-5c6d-7e8f-9a0b-1c2d3e4f5a6b", "player": { "external_ref": "0192a3b4-0000-7000-8000-000000000001", "document": { "type": "CPF", "number": "52998224725", "country": "BR" }, "name": "Jogador Teste", "email": "jogador1@nero.test", "ip": "203.0.113.10" }, "amount": { "amount": "5000", "currency": "BRL" }, "method": "pix", "expires_in_seconds": 1800 } ``` | Campo | Origem na Nero | Nota | |---|---|---| | `external_id` | Gerado pelo `nero_payments` (`dep_`) | É o `deposit_id` do front e a `Idempotency-Key` | | `player.external_ref` | Id interno imutável do jogador (IAM da Nero) | **Nunca** e-mail, telefone ou CPF | | `player.document` | CPF do cadastro, só dígitos | Só CPF ou CNPJ em PIX | | `player.name` | Nome do cadastro | **Exigido** quando a operação pede (até 120 caracteres) | | `player.email` | E-mail **cadastrado** do jogador | Só quando a operação exige (ou para risco). No hml, só `.test`, `.invalid` ou `.example` | | `player.ip` | IP do jogador que o portal recebeu do Caddy | Ajuda o risco; pode ser exigido por operação | | `amount` | Menor unidade, da conversão acima | String de inteiro | | `expires_in_seconds` | Opcional (300 a 86400; padrão 1800) | O prazo **efetivo** vem em `instructions.pix.expires_at` | | `accept_bonus` | **Não vai** ao Dracma | Bônus é regra da bet ([Arquitetura do conector](../../conector/arquitetura.md#regras-de-bonus-ficam-fora-da-api)) | Nunca envie `operation_id`, `status`, `version`, `paid_amount`, `method_options` ou `fx_quote_id`: o Dracma recusa campo desconhecido com `400` (exceto `operation_id` na raiz, que ele descarta). #### Resposta do Dracma e resposta ao front | Dracma | Outbox | Front | |---|---|---| | `201`, `status: pending` | `acknowledged`; grava `dracma_id`, `version`, `instructions` | `201 {deposit_id, status: "pending", amount, currency: "BRL", method: "pix", pix: {copy_paste, qr_code_png_base64?, expires_at}, expires_at}`; use o `expires_at` **do Dracma** | | `201`, `status: failed` | `acknowledged` (terminal) | `201 {status: "failed"}` | | `202`, `status: created` | `acknowledged` | `202 {deposit_id, status: "creating"}`; o PIX chega por `deposit.pending` | | `400 validation_failed` com `required_by_operation`, `reserved_domain_required` ou `format` | `rejected` | `422` "complete seu cadastro" (e-mail) | | Outro `400 validation_failed` | `rejected` e alerta (bug do conector) | `500` genérico ao front | | `400 currency_not_enabled` ou `method_not_enabled` | `rejected` e alerta | `503` "depósito indisponível" | | `409 duplicate_external_id` | `acknowledged` com o objeto de `existing` | Como `201` ou `202`, conforme o estado de `existing` | | `409 idempotency_key_reused` | `needs_decision` e alerta (bug) | `500` genérico | | `422 document_invalid` ou `document_mismatch` | `rejected` | `422` "confira seu CPF" | | `422 amount_below_minimum` ou `amount_above_maximum` | `rejected` | `422` com o limite (leia de `GET /v1/rules`) | | `422 player_blocked` ou `velocity_exceeded` | `rejected` | `422` "não foi possível gerar o depósito agora" (genérico, sem motivo) | | `429 rate_limited` (limite de depósitos por jogador por hora) | `pending` (repete depois do `Retry-After`), ou `rejected` se a Nero preferir desistir | `429` "muitas tentativas, aguarde" | | `503`, `5xx` ou timeout | `pending`, backoff, mesma chave | `202 {status: "creating"}` | `4xx` não cria objeto no Dracma e não se repete. Tentar de novo depois de corrigir o cadastro usa um **novo** `external_id`, portanto um novo `deposit_id`. #### `GET /api/payments/deposits/{deposit_id}` - Só o dono: `deposit_id` de outro jogador devolve `404`, a mesma resposta de "não existe". - Lê o **estado local** e traduz pela tabela de [Estados no front](estados-no-front.md). Não é proxy do Dracma. - Opcional: se o intent está parado em `creating` ou `pending` há mais de 60 segundos, faça **um** `GET /v1/deposits/{dracma_id}` (no máximo a cada 10 segundos por depósito). Sem `dracma_id` (resposta perdida), use `GET /v1/deposits?external_id=`, que está no balde de listagem (5 req/s por chave): nunca por polling do front. #### Crédito O crédito acontece **só** no caminho do webhook ou do job de `GET /events` ([Webhook, eventos e reconciliação](webhook-e-jobs.md)), depois do fetch-back, e é idempotente pelo `external_id`: ```sql -- no nero_ledger, na mesma transação que marca o evento como visto e o intent como paid INSERT INTO lancamento (origem, external_id, jogador_id, valor_minor, tipo) VALUES ('dracma', $1, $2, $3, 'credito_deposito') ON CONFLICT (origem, external_id, tipo) DO NOTHING; -- restrição única: 1 crédito por depósito ``` O valor creditado é o `amount_minor` **do intent** (o que a Nero pediu), depois de conferir que o `amount.amount` do fetch-back é idêntico. Divergência: não credita, abre `needs_decision` e dispara alerta (`nero_dracma_fetchback_mismatch_total`). ### Exemplos Os comandos para criar um depósito de teste e vê-lo pago estão em [Primeiros passos no sandbox](primeiros-passos-sandbox.md). ### Erros e casos de borda - Jogador sem e-mail cadastrado numa operação que exige: devolva "complete seu cadastro" sem chamar o Dracma, para não gastar um `external_id` recusado. - Pagamento depois de expirar ou cancelar: o fetch-back mostra `paid_amount` num estado terminal. Não credite; abra `needs_decision` (D8). - Conferência por `external_id` depois de resposta perdida: `GET /v1/deposits?external_id=` devolve exatamente um depósito ou nenhum. ### Veja também - [Estados no front](estados-no-front.md) e [Saque](saque.md). - [Convenções HTTP](../../conceitos/convencoes-http.md): conversão de valor. - [Dados de teste](../../sandbox/dados-de-teste.md). --- # Arquivo: docs/casos/nero/saque.md ## Saque no `nero_payments` > Para: Dev — Conferido em 2026-10-18. Como implementar `POST /api/wallet/withdraw/request` e `GET /api/payments/withdrawals/{withdrawal_id}` sobre o `POST /v1/withdrawals` do Dracma. A referência da API está em [Saques](../../referencia/saques.md). ### O que é A rota do portal recebe o pedido legado do front, reserva o saldo, grava o intent e chama o Dracma. As particularidades da Nero são o corpo legado (`pix_type` e `pix_key`), as mensagens genéricas de bloqueio e a pré-checagem local que só endurece. ### Quando usar Ao implementar ou revisar a rota de saque do `nero_payments`. ### Como funciona #### Reserva antes do pedido ```mermaid sequenceDiagram autonumber participant C as nero_payments participant L as nero_ledger participant D as Dracma C->>L: BEGIN, confere saldo, RESERVA (débito), grava intent wdr_uuidv7 com os bytes do corpo, COMMIT C->>D: POST /v1/withdrawals (Idempotency-Key = external_id) alt resposta conclusiva 2xx D-->>C: 201 approved, requested ou under_review C->>C: grava dracma_id, status, version, decision else 4xx conclusivo D-->>C: 400 ou 422 C->>L: devolve a reserva (uma vez, idempotente por external_id) else timeout, 5xx, 429 D--xC: sem resposta conclusiva C->>C: intent segue pending, reserva MANTIDA, worker repete com a mesma chave end ``` | O que chegou | Reserva | Por quê | |---|---|---| | `201` (qualquer estado) | Mantém | O saque existe; o desfecho vem por webhook | | `400` ou `422` | **Devolve** | `4xx` não cria objeto | | `409 duplicate_external_id` | Mantém; segue pelo `existing` | O saque já existe | | `409 idempotency_key_reused` | Mantém; `needs_decision` e alerta (D12) | Bug do conector; ninguém devolve nem reenvia sozinho | | Timeout, `5xx`, `429`, `409 idempotency_in_progress` | **Mantém**; repete com a mesma chave | Você não sabe se o saque foi criado | | Tentativas esgotadas | Mantém; `GET /v1/withdrawals?external_id=`; inconclusivo vira `needs_decision` | Nunca decida às cegas | Por que nunca devolver no timeout: se o `POST` foi aceito e a resposta se perdeu, o jogador receberia o PIX **e** o saldo de volta. #### Do corpo legado ao corpo do Dracma Pedido do front (legado): `{amount, type: "pix", pix_key, pix_type, accept_terms, currency?}`. | Campo legado | Vai ao Dracma como | Regra | |---|---|---| | `amount` (reais) | `amount.amount` na menor unidade, string | A mesma conversão sem float do [Depósito](deposito.md) | | `currency` | `amount.currency` | Ausente vale `BRL`; outra moeda, recuse | | `type` | `method` | Só `pix` | | `pix_type` | `destination.pix.key_type` | `cpf`, `cnpj`, `email`, `phone`, `random`. Mapeie legados: `evp` e `aleatoria` para `random`; `telefone` e `celular` para `phone` | | `pix_key` | `destination.pix.key` | Normalize: CPF e CNPJ só dígitos; telefone em `+55DDDNUMERO`; e-mail em minúsculas; 1 a 140 caracteres | | `accept_terms` | **Não vai** | Regra da bet: exija `true` antes de reservar | | Nenhum | `player.external_ref`, `document`, `name` | Do cadastro (asserção do portal) | | Nenhum | **`player.ip`** | IP do jogador vindo do Caddy e do portal. **Sem ele o saque nunca é automático** (`OWNERSHIP_UNVERIFIED`) | | Nenhum | `player.device_id` | Se a bet tiver | | Nenhum | `review_requested` | `true` quando a pré-checagem local desconfiar | | Nenhum | `player_context` | Opcional e informativo | ```json { "external_id": "wdr_0192a3b4-5c6d-7e8f-9a0b-1c2d3e4f5a6c", "player": { "external_ref": "0192a3b4-0000-7000-8000-000000000001", "document": { "type": "CPF", "number": "52998224725", "country": "BR" }, "name": "Jogador Teste", "ip": "203.0.113.10" }, "amount": { "amount": "5000", "currency": "BRL" }, "method": "pix", "destination": { "pix": { "key_type": "cpf", "key": "52998224725" } }, "review_requested": false } ``` A chave PIX tem de ser **do jogador**: chave `cpf` diferente do CPF do cadastro, ou `cnpj` para pessoa física, volta `422 document_mismatch`. Chaves `email`, `phone` e `random` são conferidas na consulta de titularidade pelo Dracma, o que pode levar alguns minutos (`requested`). #### Resposta do Dracma e resposta ao front | Dracma | Reserva | Front (`200` do legado) | |---|---|---| | `201 approved` ou `requested` | Mantém | `{status: true, withdrawal_id, state: "processing", message: "Saque em processamento."}` | | `201 under_review` | Mantém | `{status: true, withdrawal_id, state: "under_review", message: "Saque em análise."}` | | Timeout, `5xx` ou `429` (intent gravado) | Mantém | `{status: true, withdrawal_id, state: "processing", ...}` | | `422 amount_below_minimum` ou `amount_above_maximum` | Devolve | `{status: false, message: }` | | `422 document_invalid` ou `document_mismatch` | Devolve | `{status: false, message: "A chave PIX precisa ser do titular da conta."}` | | `422 player_blocked`, `destination_blocked` ou `velocity_exceeded` | Devolve | `{status: false, message: "Não foi possível solicitar o saque agora."}` (genérica: não revele a lista) | | `422 insufficient_operation_balance` | Devolve e alerta o financeiro | `{status: false, message: "Saque temporariamente indisponível. Tente mais tarde."}` | | `400 validation_failed` | Devolve e alerta (bug do conector) | `{status: false, message: }` | | `400 currency_not_enabled` ou `method_not_enabled` | Devolve e alerta | `{status: false, message: "Saque indisponível."}` | "Erros de negócio mantêm as mensagens atuais": use os textos que o front já mostra hoje; a tabela só sugere para os casos novos. #### Pré-checagem local: só endurece A Nero roda, antes do `POST`, uma checagem própria que conta também os depósitos anteriores ao Dracma (D3). Ela **só pode** pedir revisão: - Se desconfiou, envia `review_requested: true` e, se quiser, `review_reasons: ["texto curto"]` (aparece no painel com o prefixo `OP:`). - Nunca "libera" nada: o Dracma decide o automático com os próprios números. #### `under_review` e o desfecho - `under_review` é decidido no **painel do Dracma** por revisores da Nero (uma pessoa; duas quando o motivo é `DUAL_APPROVAL_REQUIRED`). A bet não tem rota de API para aprovar. - O desfecho chega por webhook: `approved`, `processing`, `paid` (finaliza) ou `rejected` (devolve). - Os motivos em `decision.reasons` são para o **suporte**, nunca para o jogador. Os mais comuns na Nero: `FIRST_WITHDRAWAL` (primeiro saque), `NO_SETTLED_DEPOSIT` e `AUTO_MULTIPLIER_EXCEEDED` (X = 1), `OWNERSHIP_UNVERIFIED` (faltou `player.ip`), `PIX_KEY_CHANGED` (chave nova). #### Cancelar, finalizar e devolver `POST /v1/withdrawals/{id}/cancel` (chave determinística `cnl_`) funciona em `requested`, `under_review` e em `approved` antes do envio. Depois, `409 invalid_state_transition`: espere o desfecho. Só devolva a reserva ao **ler** `cancelled` no fetch-back. No caminho do webhook, o efeito no `nero_ledger` (idempotente por `external_id`) é: | Estado lido no fetch-back | Ação | |---|---| | `paid` | Finaliza a reserva; guarda o `psp_reference` (E2E id) | | `rejected`, `failed`, `cancelled` | Devolve a reserva | | `returned` (depois de `paid`) ou `recalled` | **Nada automático**: `needs_decision` e alerta (D7, D18) | | Qualquer outro | Nada; a reserva continua | ```sql -- devolução idempotente: no máximo uma por saque INSERT INTO lancamento (origem, external_id, jogador_id, valor_minor, tipo) VALUES ('dracma', $1, $2, $3, 'devolucao_reserva_saque') ON CONFLICT (origem, external_id, tipo) DO NOTHING; ``` ### Exemplos O primeiro saque de teste, com a aprovação simulada, está em [Primeiros passos no sandbox](primeiros-passos-sandbox.md). ### Erros e casos de borda - Saque sem `player.ip`: vai sempre para revisão; verifique que o portal repassa o IP vindo do Caddy. - Chave PIX nova do jogador: o motivo `PIX_KEY_CHANGED` manda para revisão; avise o jogador de que a análise pode demorar. - Muitos saques do mesmo jogador na hora: `429 rate_limited`; mostre a mensagem genérica e não repita em laço. ### Veja também - [Estados no front](estados-no-front.md) e [Revisão e decisões](revisao-e-decisoes.md). - [Ciclo do saque](../../dinheiro/ciclo-do-saque.md). - [Decisão, limites e regras](../../dinheiro/decisao-limites-e-regras.md). --- # Arquivo: docs/casos/nero/webhook-e-jobs.md ## Webhook, eventos e reconciliação na Nero > Para: Dev — Conferido em 2026-10-18. Três rotinas mantêm o livro da Nero igual ao do Dracma: a rota do webhook, o job de `GET /events` e a reconciliação diária. Todas terminam no **mesmo** aplicador de estado. ### O que é ```mermaid flowchart LR W["Webhook: POST /api/payments/dracma/webhook"] --> A["Aplicador único"] E["Job a cada 5 min: GET /v1/events com after"] --> A R["Reconciliação diária às 02:30 UTC: GET /v1/reconciliation"] --> X["Comparação com o nero_ledger"] A --> FB["Fetch-back em /v1/deposits/id ou /v1/withdrawals/id"] FB --> L["nero_ledger: crédito, finalização ou devolução, idempotente por external_id"] X -->|"divergência"| I["Incidente e alerta"] ``` O aplicador é o ponto único da regra de dinheiro: dedup, fetch-back, coerência com o intent, versão e efeito idempotente. O desenho geral está em [Arquitetura do conector](../../conector/arquitetura.md). ### Quando usar Ao implementar o receptor e os jobs do `nero_payments`, e ao revisar o alerta de cada um. ### Como funciona #### Rota do webhook `POST /api/payments/dracma/webhook` passa pelo portal **sem** autenticação de jogador. A defesa é: 1. **Assinatura** verificada no `nero_payments` sobre o **corpo cru** (HMAC-SHA256 com o segredo de webhook, que é **diferente** do segredo de chamada). 2. **Fetch-back** antes de mover dinheiro: um segredo de webhook vazado sozinho não cria dinheiro. 3. **Caddy** da bet: o Dracma não publica lista de IPs de saída hoje; se um dia vier por contrato, aplique-a **só** nessa rota. É uma camada extra, não substitui as duas anteriores. A ordem de verificação (13 passos) e a tabela "você responde X, o Dracma faz Y" estão em [Receptor passo a passo](../../webhooks/receptor.md#os-13-passos-nesta-ordem). Na Nero: - Responda em até 10 segundos. Se o trabalho for pesado, grave o evento de forma durável e responda `200`; um worker processa. - Nunca siga redirect nem responda `3xx`: o Dracma trata como falha. - Tipo de evento ou estado desconhecido: `200`, grava para dedup, **não move dinheiro**. - Métrica `nero_dracma_webhook_verify_total{result}` com os resultados `ok`, `bad_signature`, `stale`, `mismatch_event_id` e `duplicate`. Alerta em `bad_signature` maior que 0, que pode ser ataque ou rotação mal feita. Os clientes Go e Node de [Exemplos executáveis](../../exemplos/README.md) já implementam a verificação de assinatura e passam nos vetores. #### Job de `GET /events` a cada 5 minutos ```text a cada 5 min (um único executor ativo; lock no banco): cursor = ler_cursor("dracma_events") # último evt APLICADO; nulo no primeiro run repetir até 20 páginas: r = GET /v1/events?after={cursor}&limit=200 # sem after no primeiro run se r.status == 429: esperar Retry-After; continuar se r.status >= 500 ou erro de rede: parar (tenta no próximo ciclo) para ev em r.data: aplicar_evento(ev) # MESMO aplicador do webhook, a partir do dedup cursor = ev.id; salvar_cursor(cursor) # só depois de aplicar se r.next_cursor == nulo: parar ``` - `after` tem de ser um evento **da operação** da Nero; id desconhecido dá `400` com `reason: unknown`. Guarde o cursor no banco, nunca em memória. - `GET /events` está no balde de listagem (5 req/s por chave): 20 páginas de 200 eventos por ciclo é folgado. - Os eventos de `/events` não têm assinatura própria: a autenticidade vem da chamada autenticada da Nero. - Métrica `nero_dracma_events_lag_seconds` (agora menos o `created_at` do último aplicado). Alerta se o job não roda com sucesso há mais de 15 minutos. - O poller pronto, com cursor, repetição e teste, está em [Exemplos executáveis](../../exemplos/README.md). #### Reconciliação diária ```text todo dia às 02:30 UTC, para D = ontem (UTC): itens = []; cursor = nulo repetir: r = GET /v1/reconciliation?date=D¤cy=BRL&limit=200[&cursor=...] se r.status == 409 (day_open): reagendar para depois do Retry-After; parar itens += r.items; cursor = r.next_cursor até cursor == nulo exigir sha256(linhas ordenadas por (object_id, kind)) == r.digest # senão incidente exigir Dracma-Signature da última página válida com o segredo de webhook # senão incidente comparar com o nero_ledger: todo deposit paid tem exatamente 1 crédito com o mesmo external_id e valor todo crédito da Nero tem 1 deposit paid # crédito sem par é o mais grave todo withdrawal paid corresponde a uma reserva finalizada, e vice-versa guardar o relatório e o header de assinatura como prova ``` - Divergência é **incidente**: não "conserte" o livro para bater. Congele o que for preciso e investigue pelo `trace_id` e por `GET /v1/events?object_id=`. - Compare pelo `settled_at` (UTC) do Dracma, não pela data local de Brasília: itens perto da meia-noite UTC caem no dia seguinte do calendário local. - `Digest` e `VerifyDigest` já estão nos exemplos Go e Node. - Métrica `nero_dracma_reconciliation_mismatch_total`; alerta crítico em qualquer valor maior que 0. #### Alertas mínimos da Nero Os alertas genéricos e a razão de cada um estão em [Alertas do conector](../../conector/alertas-e-observabilidade.md). Os limiares da Nero: | Sinal | Limiar | Severidade | |---|---|---| | `nero_dracma_outbox_pending` com intent mais velho que | 10 minutos | P2 | | Webhook com assinatura inválida | Mais de 0 em 5 minutos | P2 | | `401` ou `403` nas chamadas da Nero ao Dracma | Mais de 0 | P2 (credencial, relógio ou operação suspensa) | | Desvio de relógio (`server_time` de `GET /v1/ping`) | Mais de 5 segundos | P2 | | Job de `/events` sem sucesso | Mais de 15 minutos | P2 | | Fila `needs_decision` com caso parado | Mais de 4 horas aviso, mais de 24 horas crítico (D11) | P2, depois P1 | | `nero_dracma_fetchback_mismatch_total` | Mais de 0 | P1 | | Reconciliação diária com divergência | Mais de 0 | P1 | ### Exemplos O poller e o parser de erro prontos estão em [Exemplos executáveis](../../exemplos/README.md). Para provar a rota do webhook, use a [Receita 10](../../sandbox/receitas.md#receita-10-reentrega-de-webhook-teste-da-sua-deduplicacao) (reentrega) e a [Receita 11](../../sandbox/receitas.md#receita-11-webhook-forjado-nao-cria-dinheiro) (webhook forjado). ### Erros e casos de borda - O job e o webhook processam o mesmo evento: o dedup por id do evento é o que impede o crédito duplo. - Evento que chega fora de ordem: aplique só a versão maior que a guardada no intent. - Fetch-back responde `404`: responda `200` sem efeito. Rede ou `5xx`: responda `503`, e o Dracma reentrega. ### Veja também - [Receptor passo a passo](../../webhooks/receptor.md) e [Recuperação com GET /events](../../webhooks/recuperacao-get-events.md). - [Reconciliar com o seu livro](../../dinheiro/reconciliacao-do-operador.md). - [Revisão e decisões](revisao-e-decisoes.md). --- # Arquivo: docs/casos/nero/revisao-e-decisoes.md ## `under_review` e a fila `needs_decision` > Para: Dev, Adm — Conferido em 2026-10-18. Dois conceitos parecidos, com donos diferentes. Esta página mostra como a Nero os separa e quais decisões do dono definem cada caso. ### O que é | | `under_review` | `needs_decision` | |---|---|---| | Onde vive | Estado do objeto **no Dracma** | Fila **na Nero** (`nero_payments`, tela do backoffice) | | Quem decide | Revisores da Nero **no painel do Dracma** | Admin da Nero **no backoffice da Nero** (D11) | | O que decide | Se o dinheiro do Dracma vai adiante (aprovar, devolver ao pagador, recusar saque) | Se a **carteira do jogador** muda (creditar, debitar, recreditar) quando não há regra automática segura | | Dinheiro na carteira enquanto espera | Nenhum movimento (depósito não credita; saque mantém a reserva) | Nenhum movimento | A versão genérica da fila de exceções, para qualquer produto, está em [Política de exceções de dinheiro](../../conector/politica-de-excecoes.md). ### Quando usar Ao desenhar a tela de backoffice, as permissões e os alertas de caso parado, e ao treinar o time de suporte. ### Como funciona #### `under_review`: o que a Nero faz **Depósito em `under_review`** (`review_reasons`: `PAYER_MISMATCH`, `PAYER_UNKNOWN`, `AMOUNT_MISMATCH`, `PAID_AFTER_EXPIRY`, `PAYER_BLOCKED`, `RULES_UNAVAILABLE`): - A Nero **não credita**; o front mostra `under_review` ("pagamento em análise"). - A decisão é no painel do Dracma: aprovar (o depósito vai a `paid` e a Nero credita pelo fetch-back) ou devolver (vai a `returned`, e a Nero mostra `failed` com "devolvido"). - Padrão da Nero (D4): **devolver** pagamento de terceiro ou de valor diferente. - Com o Mercado Pago, `PAYER_UNKNOWN` é frequente porque o PSP não informa o documento do pagador real. Planeje gente para revisar, ou aceite que esses depósitos demorem. **Saque em `under_review`** (`decision.reasons`): - A reserva continua; o front mostra `under_review`. - A decisão é no painel do Dracma: uma pessoa, ou duas quando o motivo é `DUAL_APPROVAL_REQUIRED`. - Aprovado: `approved`, `processing`, `paid` (finaliza). Recusado: `rejected` (devolve). - A Nero pode **pedir** revisão (`review_requested: true`), mas nunca pular uma. #### `needs_decision`: quando abrir Abra um caso, dispare alerta, registre `ALERTA` no log e incremente a métrica `nero_payments_dracma_needs_decision_total` nestas situações. Em **nenhuma** delas o `nero_payments` move dinheiro sozinho. | Situação | Decisão do dono | Padrão em vigor | Ações possíveis no backoffice (D11) | |---|---|---|---| | Depósito já creditado vai a `refunded` ou `chargeback` | D6 | Não debita; `needs_decision` | `close` (perda com a casa) ou débito manual auditado pela carteira | | Saque pago vai a `returned` | D7 | Não recredita; `needs_decision`. O dinheiro voltou ao caixa da operação no Dracma | Recreditar pela carteira (auditado) ou `close`; avisar o jogador para cadastrar outra chave | | Depósito pago depois de `expired` ou `cancelled` (o fetch-back mostra `paid_amount` num estado terminal) | D8 | Não credita; `needs_decision` | `credit` **só** se o Dracma estiver em `paid` agora; senão `close` e pedir devolução no painel do Dracma | | `409` no envio de um saque (`idempotency_key_reused`, ou `duplicate_external_id` com corpo diferente) | D12 | Reserva mantida; nada devolvido nem reenviado | Investigar; `refund` só com o dinheiro comprovadamente fora do PSP | | Saque `recalled` | D18 | Não recredita | Como D7 | | Depósito `paid_partial` | Contrato | Não credita (a Nero não implementa parcial) | `credit` do valor parcial, se o dono decidir | | Fetch-back incoerente (`external_id` desconhecido, valor, moeda ou jogador diferentes do intent) | Contrato | Não credita; alerta P1 | Investigar (possível ataque ou bug) | | Outbox esgotou as tentativas e a consulta por `external_id` não resolveu | Contrato | Reserva mantida | Reprocessar com a **mesma** chave; `refund` só com prova de que o saque não existe | Ações do backoffice (D11): `credit` (depósito, só com o Dracma em `paid` agora), `refund` (saque, só com o dinheiro fora do PSP ou devolvido ao caixa), `confirm_paid` e `close` (encerra sem mover). Ler exige a permissão `payments.decisions.read`; decidir exige `payments.decisions.write`, step-up e motivo. Caso parado há mais de 4 horas é aviso; há mais de 24 horas é crítico. #### Os débitos que a Nero não automatiza ```mermaid flowchart TD A["Evento lido por fetch-back"] --> B{"Estado conhecido com regra escrita?"} B -->|"não"| Z["200, sem efeito, confere depois"] B -->|"sim"| C{"Move dinheiro a favor do jogador sem risco? paid (depósito), rejected, failed, cancelled (saque)"} C -->|"sim"| D["Aplica, idempotente por external_id"] C -->|"não: refunded, chargeback, returned, recalled, paid_partial, pago após expirar"| E["needs_decision e alerta"] ``` Por quê: um débito automático em jogador que já sacou deixa saldo negativo; um recrédito automático de saque devolvido pode pagar duas vezes se o banco reenviar. Esses casos são raros e caros: vão para uma pessoa. ### Exemplos Para provocar `refunded`, `chargeback` e `returned` no sandbox e ver o caso entrar na fila, use as [Receitas 8 e 12](../../sandbox/receitas.md). ### Erros e casos de borda - Caso aberto duas vezes para o mesmo `external_id`: a fila deve ser idempotente por objeto e por situação. - Decisão tomada no backoffice sem o motivo: recuse. O motivo é parte da trilha de auditoria. - Papel sem a permissão de decidir: a tela mostra o caso, mas não oferece as ações. ### Veja também - [Estados no front](estados-no-front.md). - [Devoluções, estornos e disputas](../../dinheiro/devolucoes-estornos-e-disputas.md). - [Revisão de depósitos e saques](../../painel/revisao-de-depositos-e-saques.md): onde o revisor decide `under_review`. --- # Arquivo: docs/casos/nero/rede-e-segredos.md ## Rede, segredos e rotação na Nero > Para: Dev, Adm — Conferido em 2026-10-18. O que a Nero configura do seu lado: onde ficam as credenciais, quem pode alcançar o Dracma, como girar chave e segredo sem parar. Os conceitos gerais estão em [Autenticação](../../api/autenticacao.md) e [Segredos e vazamento](../../seguranca/segredos-e-credenciais.md). ### O que é #### Os materiais | Material | Onde vive na Nero | Rotação | |---|---|---| | Chave de chamada `key_test_` ou `key_live_` e o segredo | Arquivo `0400` do usuário Linux do `nero_payments`, entregue pelo ops da Nero a partir do seeder da aplicação ou do cofre | Até 2 chaves ativas; a antiga é revogada depois que a nova entra em uso. Política da Nero: girar a cada 90 dias | | Segredo de webhook `whsec_` | Arquivo `0400`, **uma linha por segredo ativo** (1 ou 2) | Sobreposição de até 7 dias entre o novo e o anterior. Política da Nero: girar a cada 90 dias | | Lista de IPs de saída do Dracma | Não disponível: o Dracma não publica a lista | Não filtre a rota do webhook por IP | Nomes de variável usados pelos exemplos (adapte ao seed da Nero): | Variável | Conteúdo | |---|---| | `DRACMA_BASE_URL` | Base **com** `/v1` (hml para a operação Teste, prd para a Real). Os hosts chegam no onboarding | | `DRACMA_KEY_ID` | `key_test_` em hml, `key_live_` em prd | | `DRACMA_KEY_SECRET_FILE` | Caminho do arquivo com o segredo | | `DRACMA_WEBHOOK_SECRETS_FILE` | Caminho do arquivo com os segredos de webhook (um por linha) | **Nunca:** segredo em repositório, imagem, log, ticket, variável de front (`NEXT_PUBLIC_*`), linha de comando ou header logado inteiro (`Dracma-Signature`). O Dracma é chamado **só do servidor**. !!! warning "Não disponível" O Dracma não emite certificado de cliente (mTLS) hoje: o painel mostra que a emissão ainda não está disponível. A autenticação das chamadas é por HMAC (chave e segredo). Não há item de mTLS no go-live da Nero. Veja [Rede, allowlist e mTLS](../../seguranca/rede-allowlist-e-mtls.md#mtls). ### Quando usar Ao provisionar o servidor do `nero_payments`, ao ensaiar a rotação antes do go-live e ao responder a um vazamento. ### Como funciona #### IPs nos dois sentidos ```mermaid flowchart LR subgraph NERO["Bet Nero"] NP["nero_payments (usuário Linux próprio)"] NAT["IPs de saída (NAT)"] CAD["Caddy: rota /api/payments/dracma/webhook"] end subgraph DR["Dracma"] API["API /v1"] WH["Emissor de webhooks"] end NP --> NAT --> API WH --> CAD --> NP ``` **Nero para o Dracma.** Cadastre no painel todos os IPs de saída que o `nero_payments` pode usar (cluster, NAT redundante, DR), para que a lista fique registrada. !!! warning "Não disponível" A lista de IPs fica registrada, mas hoje não é aplicada às chamadas: uma chamada de um IP fora da lista não é recusada por isso. A defesa das chamadas é a assinatura HMAC. Veja [IPs do conector](../../painel/allowlist-de-ip.md). **Egress restrito.** Só o usuário Linux do `nero_payments` alcança o host do Dracma (nftables por UID ou cgroup). Nenhum outro serviço da bet consegue usar a credencial, mesmo se for comprometido. Esta é a defesa da Nero no lugar da lista de IPs. **Dracma para a Nero (Caddy).** A autenticidade do webhook vem da assinatura e do fetch-back. O Dracma não publica lista de IPs de saída hoje; se um dia vier por contrato, aplique-a **só** na rota do webhook, como camada extra. Dentro do serviço, confie em `X-Forwarded-For` somente quando o salto anterior for o próprio Caddy. O Dracma não garante IP fixo em hml. #### Rotação sem parar **Chave de chamada.** O pedido de chave nova precisa da aprovação de outra pessoa e de 24 horas de espera, e o segredo é revelado **uma vez** a quem pediu ([Chaves de API](../../painel/chaves-de-api.md#pedir-uma-chave-nova)). ```mermaid sequenceDiagram participant A as Admin da Nero (painel) participant A2 as Segundo aprovador participant D as Dracma participant O as ops da Nero participant C as nero_payments A->>D: pede chave nova (step-up) A2->>D: aprova Note over D: espera de 24 h D-->>A: key_NOVA e segredo, exibidos uma vez A->>O: grava no seeder ou cofre (nunca à mão no servidor) O->>C: redeploy: passa a assinar com a NOVA D-->>A: painel mostra o último uso de cada chave A->>D: revoga a ANTIGA quando o último uso tiver mais de 1 h e zero erros ``` **Segredo de webhook.** O primeiro segredo só aparece quando você aciona "Rotacionar" no painel. Em cada rotação o Dracma assina com os dois segredos por até 7 dias. Passos: (1) o ops acrescenta o segredo novo ao arquivo (duas linhas); (2) confirme webhooks aceitos (`nero_dracma_webhook_verify_total{result="ok"}`); (3) depois que a sobreposição terminar, remova o antigo. Não existe "revogar" o segredo antigo: encerrar a sobreposição é fazer uma segunda rotação ([Segredo e rotação](../../webhooks/segredos-e-rotacao.md#rotacao)). #### Vazou? 1. **Chave de chamada:** "Revogar" no painel. A revogação vale em até 30 segundos em todas as réplicas. Peça outra chave e entregue pelo ops. 2. **Segredo de webhook:** faça uma rotação e remova o anterior do arquivo quando a sobreposição terminar. Enquanto isso, o fetch-back garante que um segredo vazado não cria dinheiro. 3. **Pânico geral:** "Suspender automático" na tela de Emergência de Regras. O efeito é imediato (saque automático da moeda desligado); uma segunda pessoa precisa confirmar em até 24 horas. #### Relógio A janela de assinatura é de ±300 segundos nos dois sentidos. Use chrony com 3 ou mais fontes no host do `nero_payments`. Um job chama `GET /v1/ping` e alerta se `|server_time - agora|` passar de 5 segundos; o `cmd/ping` dos exemplos Go faz isso. ### Exemplos O ping assinado e o desvio de relógio, nos [Exemplos executáveis](../../exemplos/README.md). A verificação de que só o usuário do serviço alcança o Dracma está no item N2 do [Go-live da Nero](go-live-nero.md). ### Erros e casos de borda - Chave nova gravada à mão no servidor em vez do seeder: some no próximo redeploy do ambiente. Passe sempre pelo ops. - Duas linhas no arquivo de segredos de webhook, mas só uma é aceita: confira o fim de linha e o espaço no fim. - `401 invalid_signature` depois da troca de chave: o serviço ainda assina com a antiga ou o `DRACMA_KEY_ID` aponta para outra. ### Veja também - [Rede, allowlist e mTLS](../../seguranca/rede-allowlist-e-mtls.md) e [Segredos e vazamento](../../seguranca/segredos-e-credenciais.md). - [Chaves de API](../../painel/chaves-de-api.md) e [Segredo e rotação](../../webhooks/segredos-e-rotacao.md). - [Go-live da Nero](go-live-nero.md). --- # Arquivo: docs/casos/nero/primeiros-passos-sandbox.md ## Primeiro depósito e primeiro saque no sandbox da Nero > Para: Dev — Conferido em 2026-10-18. Roteiro para a Nero ver o ciclo inteiro funcionando na operação `nero-br-hml` (operação Teste) **antes** de escrever o conector. Nenhum dinheiro real se move. Os comandos genéricos estão em [Primeiro depósito e primeiro saque](../../onboarding/quickstart-sandbox.md); esta página traz as constantes e os resultados esperados da Nero. ### O que é Os dez passos usam o cliente de shell de [Exemplos executáveis](../../exemplos/README.md) (`bash`, `openssl`, `curl`) e `python3` ou `jq` só para ler JSON. O shell serve para **depurar**: em produção use Go ou Node, com comparação em tempo constante e segredo fora da linha de comando. ### Quando usar No primeiro dia de integração, antes de abrir qualquer rota do portal. Cada passo mostra o que o conector da Nero deve fazer com o resultado. ### Como funciona #### Passo 0: pré-requisitos - Credenciais da operação Teste: chave `key_test_` com os escopos `deposits:write`, `withdrawals:write` e `read`, o segredo num arquivo `0400` e o segredo de webhook. - URL de webhook cadastrada na operação. Fora de produção, o Dracma só entrega a domínios reservados ou liberados no onboarding: peça a liberação do domínio de hml da Nero. Sem a URL, os passos de webhook mostram só o log de entregas. - Relógio sincronizado (NTP): a janela é de ±300 segundos. - As variáveis do cliente (`DRACMA_BASE_URL`, `DRACMA_KEY_ID`, `DRACMA_KEY_SECRET_FILE`) e as funções `dr`, `js` e `novo_id` do roteiro genérico. #### Passo 1: ping ```bash dr GET /ping ``` Esperado: `200` com `operation_id`, `environment: "sandbox"` e `server_time`. Compare `server_time` com `date -u`: desvio acima de 5 segundos pede NTP. `401 unknown_key` é chave de outro tipo ou revogada; `401 invalid_signature` é segredo ou path errado; `401 timestamp_out_of_window` é relógio. #### Passo 2: moedas, métodos e exigências ```bash dr GET /currencies ``` Esperado: `BRL` com `methods: ["pix"]`. Se `deposit_requirements` for `["player.name", "player.email"]`, o PSP de depósito da operação é o Mercado Pago: os dois campos são obrigatórios e o e-mail tem de ser `.test`, `.invalid` ou `.example`. Com `["player.name"]`, é a MaximaxPay; com `[]`, o simulador. #### Passo 3: primeiro depósito ```bash DEP_EXT="dep_$(novo_id)" cat > nero-dep.json < redeliver.json dr POST /sandbox/webhooks/redeliver redeliver.json "sbx_$(novo_id)" ``` Esperado no receptor da Nero: o mesmo `Dracma-Event-Id`, resposta `200` e **nenhum** crédito novo. #### Passo 7: primeiro saque A reserva no `nero_ledger` acontece **antes** deste `POST`. O primeiro saque de um jogador costuma ir para revisão. ```bash WDR_EXT="wdr_$(novo_id)" cat > nero-wdr.json < nero-wdr2.json dr POST /withdrawals nero-wdr2.json "$WDR2_EXT" | js '["status"], v["decision"]["mode"], v["decision"]["reasons"]' ``` Esperado: `approved`, `automatic` e lista vazia. Se vier `under_review`, leia `decision.reasons`: as regras da operação (`GET /v1/rules`) podem pedir revisão por outros motivos. Sem `player.ip` o resultado é sempre `under_review` com `OWNERSHIP_UNVERIFIED`. Para ver a devolução da reserva, crie outro saque de valor terminado em `77` e chame `.../fail` ou `.../reject`. #### Passo 9: queda simulada e retomada sem duplicar ```bash echo '{"down":true}' > down.json; echo '{"down":false}' > up.json dr POST /sandbox/outage down.json "sbx_$(novo_id)" DEP2_EXT="dep_$(novo_id)"; sed "s/$DEP_EXT/$DEP2_EXT/" nero-dep.json > nero-dep2.json dr POST /deposits nero-dep2.json "$DEP2_EXT" # 503 service_unavailable, Retry-After 5 dr POST /sandbox/outage up.json "sbx_$(novo_id)" dr POST /deposits nero-dep2.json "$DEP2_EXT" # MESMA chave e corpo: 201 ou 202 dr POST /deposits nero-dep2.json "$DEP2_EXT" # mesma resposta, Idempotent-Replayed: true dr GET "/deposits?external_id=$DEP2_EXT" | js '["data"].__len__()' # 1 ``` É exatamente o comportamento que o outbox do `nero_payments` precisa ter em produção. #### Passo 10: no dia seguinte, a reconciliação ```bash ONTEM=$(date -u -d yesterday +%F) dr GET "/reconciliation?date=$ONTEM¤cy=BRL&limit=200" ``` Esperado depois das 02:00 UTC: itens `deposit` e `withdrawal` com `status: paid`, `totals`, `digest` e o header `Dracma-Signature` na última página. Antes disso: `409 day_open` com `Retry-After`. Confira o digest com `Digest` e `VerifyDigest` dos exemplos. ### Exemplos Todo o roteiro é o exemplo. Os resultados esperados de cada passo estão logo abaixo do comando. ### Erros e casos de borda | Se vier | Causa provável | |---|---| | `400` com `required_by_operation` ou `reserved_domain_required` no depósito | Falta `player.name` ou `player.email`, ou o e-mail não é de domínio reservado (passo 2) | | `422 insufficient_operation_balance` no saque | A operação ainda não tem caixa livre: pague o depósito do passo 4 antes | | `409 invalid_state_transition` no `pay` do saque | O simulador já pagou sozinho; use valor terminado em `77` para controlar | | `429 rate_limited` | Limite de depósitos ou saques por jogador por hora; use outro `external_ref` | ### Veja também - [Fluxo ponta a ponta](fluxo.md) e [Go-live da Nero](go-live-nero.md). - [Receitas por caminho](../../sandbox/receitas.md). - [Suíte de conformidade](../../sandbox/conformidade.md). --- # Arquivo: docs/casos/nero/go-live-nero.md ## Go-live da Nero: checklist com prova > Para: Dev, Adm — Conferido em 2026-10-18. A Nero só recebe a chave `key_live_` depois de cumprir esta lista **na operação `nero-br-hml`** (operação Teste). Cada item tem uma **prova**: algo que se mostra (saída de teste, log, métrica, imagem do painel), não uma promessa. Item sem prova conta como não feito. ### O que é Este checklist complementa o [Checklist de go-live](../../go-live/checklist.md), que vale para qualquer operador, com o que é específico da bet. Os números N1 a N34 são os da Nero e se mantêm estáveis, inclusive nos itens retirados. Copie para o tracker da Nero, preencha a coluna "Prova" e anexe ao registro de go-live. ### Quando usar Na semana anterior ao pedido da operação Real, e de novo no dia do corte (seção 5). ### Como funciona #### 1. Credencial e rede | # | Item | Prova | |---|---|---| | N1 | Credencial do Dracma legível **só** pelo usuário Linux do `nero_payments` (`0400`), entregue pelo ops da Nero | `stat -c '%a %U' ` mostra `400` e o usuário do `nero_payments`; busca por `key_live_` no repositório e na imagem sem resultado | | N2 | Egress para o Dracma **só** do usuário do `nero_payments` | Regra nftables versionada e teste: `curl` ao host do Dracma a partir de outro usuário falha | | N3 | Lista de IPs de saída (NAT, DR) registrada na operação | Imagem do painel com a lista e `GET /v1/ping` com `200` a partir de cada IP. A lista fica registrada mas hoje não é aplicada às chamadas; o teste prova a conectividade, não um bloqueio | | N4 | O Dracma não publica IPs de saída do webhook; se vierem por contrato, o Caddy os aplica **só** na rota do webhook | Sem lista, registre a data em que o suporte confirmou isso. Com lista, trecho do Caddyfile versionado e `curl` à rota a partir de IP fora da lista recusado no Caddy | | N5 | NTP ativo e alerta de desvio maior que 5 s via `GET /v1/ping` | `chronyc tracking` e regra de alerta versionada | | N6 | Retirado: o Dracma não emite certificado de cliente hoje | Não há prova a anexar ([Rede, allowlist e mTLS](../../seguranca/rede-allowlist-e-mtls.md#mtls)) | | N7 | Rotação de chave **e** de segredo de webhook ensaiadas no sandbox sem parar | Log do ensaio: dois segredos ativos, webhooks `ok` durante a troca, chave antiga revogada | #### 2. Depósito | # | Item | Prova | |---|---|---| | N8 | `external_id = dep_` igual ao `deposit_id` e à `Idempotency-Key`, gravado no outbox **antes** do `POST` | Teste: matar o processo entre o commit do intent e a chamada; o worker retoma com a **mesma** chave e cria 1 depósito | | N9 | Valor convertido para a menor unidade **sem float** | Teste de unidade com `50`, `50.5`, `0.01`, `50.001` (recusa) e `1e3` (recusa) | | N10 | Exigências de `GET /v1/currencies` respeitadas: `player.name` e `player.email` do cadastro quando exigidos; em hml só `.test`, `.invalid` ou `.example` | Teste: jogador sem e-mail recebe "complete o cadastro" sem chamada ao Dracma; depósito com e-mail `.test` cria `201` | | N11 | O front mostra o `expires_at` **devolvido** pelo Dracma | Imagem da tela e teste do mapeamento | | N12 | Crédito **só** em `paid` lido por fetch-back, idempotente por `external_id` | [Receita 1](../../sandbox/receitas.md#receita-1-deposito-pago-pelo-proprio-jogador-paid): 1 crédito; reentrega do mesmo evento: continua 1 | | N13 | Webhook **forjado** (assinado com o segredo de sandbox) para depósito `pending` não credita | Teste com a verificação de assinatura mais o fetch-back: `200`, 0 créditos | | N14 | `under_review` não credita; `returned` aparece como `failed` | Receitas 2 (`PAYER_MISMATCH`) e 3 (`AMOUNT_MISMATCH`) | | N15 | `paid_amount` num depósito terminal (pago depois de expirar ou cancelar) abre `needs_decision` e não credita (D8) | Teste de unidade do aplicador com um objeto `expired` e `paid_amount` | #### 3. Saque | # | Item | Prova | |---|---|---| | N16 | Reserva **antes** do `POST /withdrawals`, na mesma transação do intent `wdr_` | Teste: falha de rede no `POST`; a reserva continua, o worker repete com a mesma chave, 1 saque | | N17 | Reserva devolvida **só** em `rejected`, `failed` ou `cancelled` confirmados, ou em `4xx` | Receita 8 (`reject` e `fail`): devolução exatamente 1 vez; `503` da queda simulada não devolve | | N18 | `player.ip` do jogador em todo saque | Log do corpo enviado (sem dado pessoal) com o campo presente; saque dentro do limite sai `approved` | | N19 | Chave PIX normalizada e `pix_type` legado mapeado | Teste de unidade (`evp` para `random`, telefone em `+55...`) | | N20 | Mensagens ao jogador genéricas em bloqueio (sem revelar lista nem motivo) | Imagem das mensagens para `player_blocked` e `destination_blocked` | | N21 | `returned` e `recalled` abrem `needs_decision`, sem recrédito automático (D7, D18) | Receita 8 (`return`): caso na fila, carteira intacta | | N22 | `409 idempotency_key_reused` no envio abre `needs_decision`, reserva mantida (D12) | Teste de unidade do tratamento de resposta | #### 4. Recuperação, reconciliação e operação | # | Item | Prova | |---|---|---| | N23 | Job de `GET /events` a cada 5 minutos, cursor persistido, mesmo aplicador do webhook | Teste: receptor desligado durante um `pay`; o job credita 1 vez em até 5 minutos | | N24 | Reconciliação diária com digest **e** assinatura conferidos | Relatório de **um dia de sandbox** comparado nos dois lados, zero divergência, header guardado | | N25 | Fila `needs_decision` no backoffice com as permissões `payments.decisions.*`, step-up e motivo (D11) | Imagem da tela e teste de permissão (papel sem concessão não decide) | | N26 | Alertas de [Webhook, eventos e reconciliação](webhook-e-jobs.md) ativos | Regras versionadas e disparo de teste de cada uma | | N27 | Logs sem segredo, sem `Dracma-Signature` inteiro, sem CPF completo nem chave PIX em claro; `Dracma-Trace-Id` em toda chamada | Amostra de log revisada e busca pelos valores proibidos sem resultado | | N28 | [Suíte de conformidade](../../sandbox/conformidade.md) verde contra `nero-br-hml` | Saída do `go test -v` com todos os casos do conjunto base em `PASS`, com a data | #### 5. Dia do corte | # | Item | Prova | |---|---|---| | N29 | Front em prd sem Dracma configurado responde `503 payment_provider_not_configured` e o resto da bet funciona (D5) | Teste em hml com a credencial removida | | N30 | Rotas legadas de gateway removidas (`404`) | `curl` em cada rota da lista de [Estados no front](estados-no-front.md) | | N31 | Primeira transação real de **valor mínimo** (depósito e saque), acompanhada pelos dois lados, do `POST` à linha da reconciliação | Registro com os `external_id`, os `trace_id` e a linha do relatório do dia | | N32 | Caixa inicial conferido em `GET /v1/balances` e alerta de caixa baixo ativo | Imagem e regra de alerta | | N33 | Decisões do dono registradas: D1 (saques antigos), D6, D7, D8, D12 e D18 | Link para o registro de decisão | | N34 | Registro de go-live assinado pelos responsáveis da Nero e do Dracma | Documento assinado | !!! danger "Dinheiro real" O item N31 move dinheiro real. Use o valor mínimo da operação, com um jogador interno, e acompanhe o dinheiro nos dois livros antes de liberar o tráfego. ### Exemplos A prova de cada item de depósito e saque são as [Receitas por caminho](../../sandbox/receitas.md). O roteiro do primeiro dia está em [Primeiros passos no sandbox](primeiros-passos-sandbox.md). ### Erros e casos de borda - Um item sem prova volta para a coluna "a fazer", mesmo que o código esteja certo. - Prova antiga (de antes de uma mudança de regras, de PSP ou de segredo) não vale: refaça. - Se o Dracma mudar o contrato (campo novo, estado novo), repita os itens N12, N14, N21 e N28. ### Veja também - [Checklist de go-live](../../go-live/checklist.md) e [Dia do corte e rollback](../../go-live/dia-do-corte-e-rollback.md). - [De Teste para Real](../../onboarding/promocao-para-producao.md). - [Revisão e decisões](revisao-e-decisoes.md). --- # Arquivo: docs/exemplos/vetores-assinatura.json ```json { "descricao": "Vetores normativos de CONTRATO.md §3 e §4. Toda implementação (Dracma, falso da bet, conector) DEVE reproduzir byte a byte.", "chamada": [ { "segredo": "sk_test_vetor_0123456789abcdef", "key_id": "key_test_vetor", "timestamp": "1790000000", "nonce": "0123456789abcdef0123456789abcdef", "metodo": "POST", "path_com_query": "/v1/deposits", "corpo": "{\"external_id\":\"dep_0192a3b4-5c6d-7e8f-9a0b-1c2d3e4f5a6b\",\"player\":{\"external_ref\":\"u-1\",\"document\":{\"type\":\"CPF\",\"number\":\"12345678909\",\"country\":\"BR\"}},\"amount\":{\"amount\":\"5000\",\"currency\":\"BRL\"},\"method\":\"pix\"}", "sha256_corpo": "fed01c42dbe44bee43439f86a259262975544b122ac57e75977fb5520bbb6edb", "pedido_canonico": "v1\n1790000000\n0123456789abcdef0123456789abcdef\nPOST\n/v1/deposits\nfed01c42dbe44bee43439f86a259262975544b122ac57e75977fb5520bbb6edb", "Dracma-Signature": "v1=a58fa1956941157aad72536512b2b09f234de3670050c865071ddc56df3b4964" }, { "segredo": "sk_test_vetor_0123456789abcdef", "key_id": "key_test_vetor", "timestamp": "1790000123", "nonce": "ffffffffffffffffffffffffffffffff", "metodo": "GET", "path_com_query": "/v1/deposits?external_id=dep_x&limit=10", "corpo": "", "sha256_corpo": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "pedido_canonico": "v1\n1790000123\nffffffffffffffffffffffffffffffff\nGET\n/v1/deposits?external_id=dep_x&limit=10\ne3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "Dracma-Signature": "v1=83b5e8b97bccfc97db1fdaa7f847b2a229a0835b88ce95032ab7c8f951487316" }, { "segredo": "sk_test_vetor_0123456789abcdef", "key_id": "key_test_vetor", "timestamp": "1790000200", "nonce": "00000000000000000000000000000001", "metodo": "POST", "path_com_query": "/v1/withdrawals/wdr_01J9ZQ4/cancel", "corpo": "", "sha256_corpo": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "pedido_canonico": "v1\n1790000200\n00000000000000000000000000000001\nPOST\n/v1/withdrawals/wdr_01J9ZQ4/cancel\ne3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "Dracma-Signature": "v1=64df5c1a75e4cfc367e165f55be0d6b50678848069f96abb27a4f2559abdae5d" } ], "webhook": [ { "segredo": "whsec_test_vetor_fedcba9876543210", "timestamp": "1790000300", "corpo": "{\"id\":\"evt_01J9ZQ5\",\"type\":\"deposit.paid\",\"created_at\":\"2026-09-26T15:00:00Z\",\"operation_id\":\"op_nero_hml\",\"object_id\":\"dep_01J9ZQ3\",\"object_version\":3,\"data\":{\"object\":{\"id\":\"dep_01J9ZQ3\",\"external_id\":\"dep_0192a3b4-5c6d-7e8f-9a0b-1c2d3e4f5a6b\",\"status\":\"paid\"}}}", "Dracma-Signature": "t=1790000300,v1=4f82d5dac571d1a7b4dae9ef852c74cffcb6a4ce76f7f3209d685798e02a229b" }, { "segredos_validos": [ "whsec_test_vetor_fedcba9876543210", "whsec_test_vetor_ANTERIOR" ], "nota": "rotação: dois v1; aceitar se QUALQUER um bater", "timestamp": "1790000300", "corpo": "{\"id\":\"evt_01J9ZQ5\",\"type\":\"deposit.paid\",\"created_at\":\"2026-09-26T15:00:00Z\",\"operation_id\":\"op_nero_hml\",\"object_id\":\"dep_01J9ZQ3\",\"object_version\":3,\"data\":{\"object\":{\"id\":\"dep_01J9ZQ3\",\"external_id\":\"dep_0192a3b4-5c6d-7e8f-9a0b-1c2d3e4f5a6b\",\"status\":\"paid\"}}}", "Dracma-Signature": "t=1790000300,v1=4f82d5dac571d1a7b4dae9ef852c74cffcb6a4ce76f7f3209d685798e02a229b,v1=f146fd13a7702bb83873e1f2ff96947c854086637cc252e536b18f3dd6569920" }, { "segredo": "whsec_test_vetor_fedcba9876543210", "nota": "NEGATIVO: corpo alterado em 1 byte com a assinatura do primeiro vetor → DEVE recusar", "timestamp": "1790000300", "corpo": "{\"id\":\"evt_01J9ZQ5\",\"type\":\"deposit.paid\",\"created_at\":\"2026-09-26T15:00:00Z\",\"operation_id\":\"op_nero_hml\",\"object_id\":\"dep_01J9ZQ3\",\"object_version\":4,\"data\":{\"object\":{\"id\":\"dep_01J9ZQ3\",\"external_id\":\"dep_0192a3b4-5c6d-7e8f-9a0b-1c2d3e4f5a6b\",\"status\":\"paid\"}}}", "Dracma-Signature": "t=1790000300,v1=4f82d5dac571d1a7b4dae9ef852c74cffcb6a4ce76f7f3209d685798e02a229b", "valido": false } ] } ``` --- # Arquivo: docs/referencia/openapi.yaml ```yaml openapi: 3.1.0 info: title: Dracma — API de pagamentos para operadores (v1) version: 1.1.0 summary: Gateway oficial multitenant de cash-in/cash-out (PIX, cartão, cripto, wire, SWIFT) com motor de regras. description: | Contrato NORMATIVO entre um operador (ex.: a bet "Nero") e o Dracma, o gateway oficial que intermedeia todos os pagamentos. A semântica que o YAML não expressa — algoritmo de assinatura, janela anti-replay, idempotência, máquinas de estado, ordem de webhooks, fetch-back — está em `CONTRATO.md`, que é parte deste contrato. Convenções que valem para TODO o documento: - Dinheiro é SEMPRE `Money`: `amount` é string de inteiro na MENOR unidade da moeda (centavo, satoshi, wei) e `currency` é o código do registro de moedas (`GET /v1/currencies`). Nunca float, nunca decimal com vírgula. - Tempo é RFC 3339 em UTC (`2026-09-26T15:04:05Z`). - IDs do Dracma são opacos com prefixo (`dep_`, `wdr_`, `evt_`, `rcn_`, `chg_`; v1.1: `rcv_` recebimento bancário, `fxq_` cotação de câmbio). O operador nunca deriva nada do formato. - Cotação/taxa de câmbio é decimal em STRING (`"5.4321"`), nunca float. Data de liquidação é `date` (AAAA-MM-DD) no calendário do país do trilho. - LEITOR TOLERANTE (v1.1.0, CONTRATO.md §6.0): o operador DEVE aceitar valor desconhecido de `status`, `type` de evento, `Metodo`, `rail`, `kind` e campo desconhecido; estado/evento desconhecido NUNCA move dinheiro no operador — é não-terminal e se confere depois pelo fetch-back. - O TENANT e a OPERAÇÃO vêm da credencial (certificado mTLS + chave de API), NUNCA do corpo. Um campo `operation_id` no corpo é ignorado se existir. - Erro é `application/problem+json` (RFC 9457) com `code` estável. - Rotas `/sandbox/*` só existem fora de produção (ver CONTRATO.md §9). DOCUMENTO SERVIDO pelo `dracma_api` (`GET /openapi.yaml`, público, sem autenticação): é a cópia do contrato 1.1.0 recortada ao que a API IMPLEMENTA hoje (PIX) mais os acréscimos ADITIVOS já em produção (`Moeda.method_details`, motivos de `Problema.errors[].reason`, 404 no cancel, `Idempotency-Key` nas rotas `/sandbox/*`, ações de sandbox `refund`/`chargeback` do depósito). As rotas da superfície wire/SWIFT/câmbio da v1.1.0 estão servidas no recorte PIX (P056): nenhuma operação tem câmbio nem trilho bancário habilitado, então `GET /fx/quotes/{id}` e `GET /receipts/{id}` → 404, `GET /receipts` → página vazia, `POST /withdrawals/{id}/recall` → 409 `invalid_state_transition` (saque PIX não tem recall) e `POST /sandbox/bank-transfers/incoming` → 400 `rail_not_enabled`. Cada rota daqui está registrada no roteador v1 e vice-versa (teste de compatibilidade no CI). O diff contra o contrato do briefing fica em `dracma_api/docs/openapi-divergencias.md`. license: name: Proprietário — uso interno entre as partes servers: - url: https://api.dracma.example/v1 description: Produção (mTLS obrigatório) - url: https://sandbox.dracma.example/v1 description: Sandbox (nenhum dinheiro real se move; sink por construção) security: - mtls: [] assinaturaHMAC: [] tags: - name: depositos - name: saques - name: jogadores - name: operacao - name: regras - name: reconciliacao - name: cambio description: v1.1.0 — cotações de câmbio (FX) para depósito/saque com moeda de pagamento diferente da carteira. - name: recebimentos description: v1.1.0 — créditos bancários (wire/SWIFT) recebidos sem casar com um depósito. Somente leitura pela API. - name: sandbox description: Superfície de controle SÓ de não-produção. Em prd responde 404. paths: /ping: get: tags: [operacao] operationId: ping summary: Verifica conectividade e credencial (sem efeito). responses: "200": description: Credencial válida. content: application/json: schema: type: object required: [ok, operation_id, environment, server_time] properties: ok: { type: boolean, const: true } operation_id: { type: string } environment: { type: string, enum: [production, sandbox] } server_time: { type: string, format: date-time } "401": { $ref: "#/components/responses/NaoAutenticado" } /currencies: get: tags: [operacao] operationId: listarMoedas summary: Registro de moedas e ativos habilitados para a operação. responses: "200": description: Moedas habilitadas. content: application/json: schema: type: object required: [data] properties: data: type: array items: { $ref: "#/components/schemas/Moeda" } /deposits: post: tags: [depositos] operationId: criarDeposito summary: Cria uma intenção de depósito e devolve as instruções de pagamento. description: | Idempotente por `Idempotency-Key` (obrigatório) E por `external_id` (único por operação). A mesma chave com o mesmo corpo devolve a MESMA resposta; com corpo diferente, 409 `idempotency_key_reused`. Pode responder 202 com `status=created` e sem `instructions` quando o PSP ainda não devolveu o PIX/checkout: o operador consulta ou espera o webhook `deposit.pending`. v1.1.0 — `wire`/`swift`: as instruções trazem a conta de recebimento e uma `reference` OBRIGATÓRIA que o pagador DEVE pôr na transferência; sem ela o crédito vira `Recebimento` não casado (CONTRATO.md §6.4). parameters: - $ref: "#/components/parameters/IdempotencyKey" requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/CriarDeposito" } examples: pix: value: 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@test.nerobet.lol amount: { amount: "5000", currency: BRL } method: pix expires_in_seconds: 1800 sepa: summary: v1.1.0 — depósito por transferência SEPA (EUR) value: external_id: dep_0192a3b4-5c6d-7e8f-9a0b-1c2d3e4f5a70 player: external_ref: 0192a3b4-0000-7000-8000-000000000002 document: { type: NATIONAL_ID, number: "X1234567", country: ES } name: Jugador Prueba amount: { amount: "10000", currency: EUR } method: wire method_options: wire: { rail: sepa_ct, reference_valid_days: 10 } responses: "201": description: Depósito criado com instruções. content: application/json: schema: { $ref: "#/components/schemas/Deposito" } "202": description: Depósito aceito; instruções ainda não disponíveis. content: application/json: schema: { $ref: "#/components/schemas/Deposito" } "400": { $ref: "#/components/responses/Invalido" } "401": { $ref: "#/components/responses/NaoAutenticado" } "403": { $ref: "#/components/responses/Proibido" } "409": { $ref: "#/components/responses/Conflito" } "422": { $ref: "#/components/responses/RegraDeNegocio" } "429": { $ref: "#/components/responses/Limitado" } "503": { $ref: "#/components/responses/Indisponivel" } get: tags: [depositos] operationId: listarDepositos summary: Lista depósitos da operação (cursor). parameters: - { name: external_id, in: query, schema: { type: string } } - { name: player_ref, in: query, schema: { type: string } } - { name: status, in: query, schema: { $ref: "#/components/schemas/StatusDeposito" } } - { name: updated_since, in: query, schema: { type: string, format: date-time } } - $ref: "#/components/parameters/Cursor" - $ref: "#/components/parameters/Limite" responses: "200": description: Página de depósitos. content: application/json: schema: type: object required: [data, next_cursor] properties: data: { type: array, items: { $ref: "#/components/schemas/Deposito" } } next_cursor: { type: [string, "null"] } /deposits/{id}: get: tags: [depositos] operationId: obterDeposito summary: Estado autoritativo de um depósito (usado no fetch-back do webhook). parameters: - $ref: "#/components/parameters/IdDeposito" responses: "200": description: Depósito. content: application/json: schema: { $ref: "#/components/schemas/Deposito" } "404": { $ref: "#/components/responses/NaoEncontrado" } /deposits/{id}/cancel: post: tags: [depositos] operationId: cancelarDeposito summary: Cancela um depósito ainda não pago. Idempotente. parameters: - $ref: "#/components/parameters/IdDeposito" - $ref: "#/components/parameters/IdempotencyKey" responses: "200": description: Depósito cancelado (ou já estava cancelado). content: application/json: schema: { $ref: "#/components/schemas/Deposito" } "409": { $ref: "#/components/responses/Conflito" } "404": { $ref: "#/components/responses/NaoEncontrado" } /withdrawals: post: tags: [saques] operationId: criarSaque summary: Pede um saque; o motor de regras decide automático ou revisão. description: | O operador JÁ reservou (debitou) o valor do jogador antes de chamar. A resposta traz `status` = `approved`/`processing` (automático) ou `under_review` (manual), com `decision.reasons`. Se `review_requested` vier `true`, o Dracma DEVE mandar para revisão manual — o operador só pode tornar a decisão MAIS restritiva, nunca menos. `player_context` é INFORMATIVO: o Dracma nunca automatiza com base nele; a regra X× usa os depósitos que o PRÓPRIO Dracma liquidou. parameters: - $ref: "#/components/parameters/IdempotencyKey" requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/CriarSaque" } examples: pix: value: 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 amount: { amount: "5000", currency: BRL } method: pix destination: pix: { key_type: cpf, key: "12345678909" } review_requested: false swift: summary: v1.1.0 — saque SWIFT em USD de carteira USD value: external_id: wdr_0192a3b4-5c6d-7e8f-9a0b-1c2d3e4f5a71 player: external_ref: 0192a3b4-0000-7000-8000-000000000003 document: { type: PASSPORT, number: "P1234567", country: GB } name: Test Player amount: { amount: "250000", currency: USD } method: swift destination: swift: charge_bearer: SHA account: holder_name: Test Player holder_type: individual holder_address: { line1: "1 Example Street", city: London, postal_code: "EC1A 1AA", country: GB } country: GB iban: GB82WEST12345698765432 bic: WESTGB2LXXX purpose: { code: gaming_winnings } review_requested: false responses: "201": description: Saque registrado com decisão. content: application/json: schema: { $ref: "#/components/schemas/Saque" } "400": { $ref: "#/components/responses/Invalido" } "401": { $ref: "#/components/responses/NaoAutenticado" } "403": { $ref: "#/components/responses/Proibido" } "409": { $ref: "#/components/responses/Conflito" } "422": { $ref: "#/components/responses/RegraDeNegocio" } "429": { $ref: "#/components/responses/Limitado" } "503": { $ref: "#/components/responses/Indisponivel" } get: tags: [saques] operationId: listarSaques summary: Lista saques da operação (cursor). parameters: - { name: external_id, in: query, schema: { type: string } } - { name: player_ref, in: query, schema: { type: string } } - { name: status, in: query, schema: { $ref: "#/components/schemas/StatusSaque" } } - { name: updated_since, in: query, schema: { type: string, format: date-time } } - $ref: "#/components/parameters/Cursor" - $ref: "#/components/parameters/Limite" responses: "200": description: Página de saques. content: application/json: schema: type: object required: [data, next_cursor] properties: data: { type: array, items: { $ref: "#/components/schemas/Saque" } } next_cursor: { type: [string, "null"] } /withdrawals/{id}: get: tags: [saques] operationId: obterSaque summary: Estado autoritativo de um saque (usado no fetch-back do webhook). parameters: - $ref: "#/components/parameters/IdSaque" responses: "200": description: Saque. content: application/json: schema: { $ref: "#/components/schemas/Saque" } "404": { $ref: "#/components/responses/NaoEncontrado" } /withdrawals/{id}/cancel: post: tags: [saques] operationId: cancelarSaque summary: Cancela um saque que ainda NÃO foi enviado ao PSP (requested/under_review/approved). parameters: - $ref: "#/components/parameters/IdSaque" - $ref: "#/components/parameters/IdempotencyKey" responses: "200": description: Saque cancelado (ou já estava). content: application/json: schema: { $ref: "#/components/schemas/Saque" } "409": { $ref: "#/components/responses/Conflito" } "404": { $ref: "#/components/responses/NaoEncontrado" } /withdrawals/{id}/recall: post: tags: [saques] operationId: pedirRecallDeSaque summary: v1.1.0 — Pede o recall (devolução) de um saque wire/SWIFT já enviado. description: | Só para `wire`/`swift` em `sent`, `in_transit` ou `paid`, dentro da janela de recall do trilho (METODOS.md). Leva a `recall_requested`; o banco do beneficiário decide: `recalled` (o dinheiro volta ao CAIXA DA OPERAÇÃO, não ao jogador — DECISOES D18) ou volta ao estado anterior com evento `withdrawal.recall_rejected`. Recall é PEDIDO, não garantia. Escopo `withdrawals:write`. Idempotente. parameters: - $ref: "#/components/parameters/IdSaque" - $ref: "#/components/parameters/IdempotencyKey" requestBody: required: true content: application/json: schema: type: object required: [reason_code] additionalProperties: false properties: reason_code: { type: string, enum: [duplicate, wrong_beneficiary, wrong_amount, fraud_suspected, other] } justification: { type: string, maxLength: 1000 } responses: "202": description: Pedido de recall registrado. content: application/json: schema: { $ref: "#/components/schemas/Saque" } "400": { $ref: "#/components/responses/Invalido" } "404": { $ref: "#/components/responses/NaoEncontrado" } "409": { $ref: "#/components/responses/Conflito" } "422": { $ref: "#/components/responses/RegraDeNegocio" } /fx/quotes: post: tags: [cambio] operationId: criarCotacao summary: v1.1.0 — Cota câmbio para um depósito/saque (sem efeito em dinheiro). description: | Devolve uma `Cotacao` válida até `expires_at`. Usar o `id` em `fx_quote_id` do depósito/saque trava a taxa (CONTRATO.md §6.5). Exige escopo `deposits:write` ou `withdrawals:write`, conforme `purpose`. Não consome saldo; cotação não usada simplesmente expira. parameters: - $ref: "#/components/parameters/IdempotencyKey" requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/PedidoCotacao" } responses: "201": description: Cotação emitida. content: application/json: schema: { $ref: "#/components/schemas/Cotacao" } "400": { $ref: "#/components/responses/Invalido" } "401": { $ref: "#/components/responses/NaoAutenticado" } "403": { $ref: "#/components/responses/Proibido" } "422": { $ref: "#/components/responses/RegraDeNegocio" } "503": { $ref: "#/components/responses/Indisponivel" } /fx/quotes/{id}: get: tags: [cambio] operationId: obterCotacao summary: v1.1.0 — Consulta uma cotação de câmbio. parameters: - { name: id, in: path, required: true, schema: { type: string, pattern: "^fxq_[A-Za-z0-9]+$" } } responses: "200": description: Cotação. content: application/json: schema: { $ref: "#/components/schemas/Cotacao" } "404": { $ref: "#/components/responses/NaoEncontrado" } /receipts: get: tags: [recebimentos] operationId: listarRecebimentos summary: v1.1.0 — Créditos bancários recebidos que NÃO casaram com um depósito. description: | Somente leitura. Casar um recebimento com um depósito ou devolvê-lo é ação do PAINEL (PAINEL.md §5.1), nunca da API: uma credencial vazada não pode apontar dinheiro recebido para um jogador escolhido. parameters: - { name: status, in: query, schema: { $ref: "#/components/schemas/StatusRecebimento" } } - { name: updated_since, in: query, schema: { type: string, format: date-time } } - $ref: "#/components/parameters/Cursor" - $ref: "#/components/parameters/Limite" responses: "200": description: Página de recebimentos. content: application/json: schema: type: object required: [data, next_cursor] properties: data: { type: array, items: { $ref: "#/components/schemas/Recebimento" } } next_cursor: { type: [string, "null"] } "400": { $ref: "#/components/responses/Invalido" } /receipts/{id}: get: tags: [recebimentos] operationId: obterRecebimento summary: v1.1.0 — Consulta um recebimento bancário não casado. parameters: - { name: id, in: path, required: true, schema: { type: string, pattern: "^rcv_[A-Za-z0-9]+$" } } responses: "200": description: Recebimento. content: application/json: schema: { $ref: "#/components/schemas/Recebimento" } "404": { $ref: "#/components/responses/NaoEncontrado" } /players/{external_ref}/limits: get: tags: [jogadores] operationId: limitesDoJogador summary: Posição do jogador perante as regras (quanto ainda saca no automático). parameters: - { name: external_ref, in: path, required: true, schema: { type: string, maxLength: 64 } } - { name: currency, in: query, required: true, schema: { type: string } } responses: "200": description: Posição. content: application/json: schema: { $ref: "#/components/schemas/LimitesJogador" } "404": { $ref: "#/components/responses/NaoEncontrado" } /balances: get: tags: [operacao] operationId: saldosDaOperacao summary: Saldo de liquidação da operação por moeda (o "caixa" para pagar saques). responses: "200": description: Saldos. content: application/json: schema: type: object required: [data] properties: data: type: array items: type: object required: [available, pending_in, pending_out, as_of] properties: available: { $ref: "#/components/schemas/Money" } pending_in: { $ref: "#/components/schemas/Money" } pending_out: { $ref: "#/components/schemas/Money" } as_of: { type: string, format: date-time } /rules: get: tags: [regras] operationId: obterRegras summary: Conjunto de regras VIGENTE da operação (somente leitura pela API). description: | A API do operador NÃO altera regra diretamente: uma credencial vazada não pode subir o próprio limite. Mudança = `POST /rules/change-requests`, que só vale depois de aprovada no painel por DUAS pessoas (REGRAS.md §9). responses: "200": description: Regras vigentes. headers: ETag: { schema: { type: string } } content: application/json: schema: { $ref: "#/components/schemas/ConjuntoDeRegras" } /rules/change-requests: post: tags: [regras] operationId: pedirMudancaDeRegras summary: Propõe mudança de regra; fica pendente de aprovação dupla no painel. parameters: - $ref: "#/components/parameters/IdempotencyKey" - { name: If-Match, in: header, required: true, schema: { type: string } } requestBody: required: true content: application/json: schema: type: object required: [rules, justification] properties: rules: { $ref: "#/components/schemas/ConjuntoDeRegras" } justification: { type: string, maxLength: 1000 } responses: "202": description: Pedido registrado, aguardando aprovação. content: application/json: schema: type: object required: [id, status] properties: id: { type: string, examples: [chg_01J9ZQ] } status: { type: string, enum: [pending_approval] } "412": { $ref: "#/components/responses/Conflito" } /reconciliation: get: tags: [reconciliacao] operationId: reconciliacao summary: Movimentos liquidados num dia (UTC), com totais e digest assinado. parameters: - { name: date, in: query, required: true, schema: { type: string, format: date } } - { name: currency, in: query, required: true, schema: { type: string } } - $ref: "#/components/parameters/Cursor" - $ref: "#/components/parameters/Limite" responses: "200": description: Relatório do dia (paginado; totais e digest só na última página). content: application/json: schema: { $ref: "#/components/schemas/Reconciliacao" } "409": description: Dia ainda aberto (a janela fecha às 00:00 UTC + 2h de carência). content: application/problem+json: schema: { $ref: "#/components/schemas/Problema" } /events: get: tags: [operacao] operationId: listarEventos summary: Replay de eventos (mesmo envelope do webhook), para recuperar webhook perdido. parameters: - { name: after, in: query, schema: { type: string }, description: "id de evento; devolve os posteriores" } - { name: object_id, in: query, schema: { type: string } } - $ref: "#/components/parameters/Limite" responses: "200": description: Eventos em ordem de criação. content: application/json: schema: type: object required: [data, next_cursor] properties: data: { type: array, items: { $ref: "#/components/schemas/Evento" } } next_cursor: { type: [string, "null"] } # ---------- Superfície de CONTROLE do sandbox (só não-prd) ---------- /sandbox/deposits/{id}/pay: post: tags: [sandbox] operationId: sandboxPagarDeposito summary: Simula o pagador pagando o PIX/cartão/cripto. parameters: - $ref: "#/components/parameters/IdDeposito" - $ref: "#/components/parameters/IdempotencyKey" requestBody: content: application/json: schema: type: object properties: payer_document: { $ref: "#/components/schemas/Documento" } paid_amount: { $ref: "#/components/schemas/Money" } responses: "200": { description: Aplicado; webhook disparado., content: { application/json: { schema: { $ref: "#/components/schemas/Deposito" } } } } /sandbox/deposits/{id}/expire: post: tags: [sandbox] operationId: sandboxExpirarDeposito summary: Simula a expiração de um depósito pendente. parameters: [ { $ref: "#/components/parameters/IdDeposito" }, { $ref: "#/components/parameters/IdempotencyKey" } ] responses: "200": { description: Aplicado., content: { application/json: { schema: { $ref: "#/components/schemas/Deposito" } } } } /sandbox/deposits/{id}/refund: post: tags: [sandbox] operationId: sandboxEstornarDeposito summary: ADITIVO (dracma_api) — Simula o estorno de um depósito pago (`paid → refunded`, CONTRATO.md §6.1). description: | Sobe `version` e dispara o evento `deposit.refunded` (webhook) na mesma transação. Corpo vazio (ou `{}`). Fora de `paid` → 409 `invalid_state_transition`. Em produção → 404. parameters: [ { $ref: "#/components/parameters/IdDeposito" }, { $ref: "#/components/parameters/IdempotencyKey" } ] responses: "200": { description: Aplicado; webhook disparado., content: { application/json: { schema: { $ref: "#/components/schemas/Deposito" } } } } "404": { $ref: "#/components/responses/NaoEncontrado" } "409": { $ref: "#/components/responses/Conflito" } /sandbox/deposits/{id}/chargeback: post: tags: [sandbox] operationId: sandboxChargebackDeposito summary: ADITIVO (dracma_api) — Simula MED/chargeback de um depósito pago (`paid → chargeback`, CONTRATO.md §6.1). description: | Sobe `version` e dispara o evento `deposit.chargeback` (webhook) na mesma transação. Corpo vazio (ou `{}`). Fora de `paid` → 409 `invalid_state_transition`. Em produção → 404. parameters: [ { $ref: "#/components/parameters/IdDeposito" }, { $ref: "#/components/parameters/IdempotencyKey" } ] responses: "200": { description: Aplicado; webhook disparado., content: { application/json: { schema: { $ref: "#/components/schemas/Deposito" } } } } "404": { $ref: "#/components/responses/NaoEncontrado" } "409": { $ref: "#/components/responses/Conflito" } /sandbox/withdrawals/{id}/{action}: post: tags: [sandbox] operationId: sandboxAgirNoSaque summary: Simula a decisão manual e o resultado do PSP. parameters: - $ref: "#/components/parameters/IdSaque" - $ref: "#/components/parameters/IdempotencyKey" - name: action in: path required: true description: | v1.1.0 acrescenta (wire/SWIFT): `compliance_hold`, `compliance_clear`, `compliance_reject`, `send`, `transit`, `recall_accept`, `recall_reject`. `return` passa a valer também a partir de `sent`/`in_transit`. schema: type: string enum: [approve, reject, pay, fail, return, compliance_hold, compliance_clear, compliance_reject, send, transit, recall_accept, recall_reject] responses: "200": { description: Aplicado., content: { application/json: { schema: { $ref: "#/components/schemas/Saque" } } } } "409": { $ref: "#/components/responses/Conflito" } /sandbox/bank-transfers/incoming: post: tags: [sandbox] operationId: sandboxTransferenciaRecebida summary: v1.1.0 — Simula um crédito wire/SWIFT chegando na conta de recebimento. parameters: [ { $ref: "#/components/parameters/IdempotencyKey" } ] description: | Com `reference` de um depósito `pending`, casa com ele (valor igual → `paid`; diferente → `under_review`); sem referência, com referência desconhecida ou de depósito expirado, cria um `Recebimento` `unmatched`. `screening: hit` força `pending_compliance`/`frozen`. requestBody: required: true content: application/json: schema: type: object required: [amount, rail] additionalProperties: false properties: amount: { $ref: "#/components/schemas/Money" } rail: { $ref: "#/components/schemas/Trilho" } reference: { type: string, maxLength: 140 } payer_name: { type: string, maxLength: 140 } payer_document: { $ref: "#/components/schemas/Documento" } payer_country: { type: string, pattern: "^[A-Z]{2}$" } screening: { type: string, enum: [clear, hit], default: clear } responses: "400": { $ref: "#/components/responses/Invalido" } "200": description: Aplicado; devolve o objeto afetado. content: application/json: schema: oneOf: - { $ref: "#/components/schemas/Deposito" } - { $ref: "#/components/schemas/Recebimento" } /sandbox/outage: post: tags: [sandbox] operationId: sandboxQueda summary: Liga/desliga a simulação de indisponibilidade (toda rota não-sandbox responde 503). parameters: [ { $ref: "#/components/parameters/IdempotencyKey" } ] requestBody: required: true content: application/json: schema: type: object required: [down] properties: down: { type: boolean } responses: "200": { description: Aplicado. } /sandbox/webhooks: get: tags: [sandbox] operationId: sandboxWebhooksEnviados summary: Log das entregas de webhook (evento, tentativa, status HTTP recebido). responses: "200": { description: Log., content: { application/json: { schema: { type: object } } } } /sandbox/webhooks/redeliver: post: tags: [sandbox] operationId: sandboxReentregar summary: Reentrega um evento já enviado (testa a deduplicação do operador). parameters: [ { $ref: "#/components/parameters/IdempotencyKey" } ] requestBody: required: true content: application/json: schema: type: object required: [event_id] properties: event_id: { type: string } responses: "200": { description: Reentregue. } webhooks: evento: post: summary: Mudança de estado de depósito/saque/regra, entregue ao operador. description: | POST na URL cadastrada da operação. Headers `Dracma-Event-Id`, `Dracma-Timestamp`, `Dracma-Signature` (CONTRATO.md §4). O operador responde 2xx em até 10 s; qualquer outra coisa = nova tentativa com backoff (CONTRATO.md §5). O operador NÃO credita pelo corpo do webhook: faz fetch-back (`GET /deposits/{id}`) e age pelo estado autoritativo. v1.1.0: tipo de evento desconhecido → responder 200 e NÃO mover dinheiro (CONTRATO.md §6.0). requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/Evento" } responses: "200": { description: Recebido (inclusive duplicata). } "401": { description: Assinatura/timestamp inválido — o Dracma NÃO insiste além da política normal e alerta. } components: securitySchemes: mtls: type: mutualTLS description: Certificado de cliente por operação, emitido pela CA do Dracma. Obrigatório em produção. assinaturaHMAC: type: apiKey in: header name: Dracma-Signature description: | HMAC-SHA256 do pedido canônico com o segredo da chave `Dracma-Key-Id`, mais `Dracma-Timestamp` e `Dracma-Nonce`. Algoritmo em CONTRATO.md §3. parameters: IdempotencyKey: name: Idempotency-Key in: header required: true schema: { type: string, minLength: 16, maxLength: 128, pattern: "^[A-Za-z0-9_.:-]+$" } description: Retida por 30 dias. Mesma chave + mesmo corpo = mesma resposta; corpo diferente = 409. IdDeposito: name: id in: path required: true schema: { type: string, pattern: "^dep_[A-Za-z0-9]+$" } IdSaque: name: id in: path required: true schema: { type: string, pattern: "^wdr_[A-Za-z0-9]+$" } Cursor: name: cursor in: query schema: { type: string } Limite: name: limit in: query schema: { type: integer, minimum: 1, maximum: 200, default: 50 } responses: Invalido: description: Corpo/parâmetro inválido. content: { application/problem+json: { schema: { $ref: "#/components/schemas/Problema" } } } NaoAutenticado: description: Credencial/assinatura/timestamp/nonce inválido. content: { application/problem+json: { schema: { $ref: "#/components/schemas/Problema" } } } Proibido: description: Credencial válida sem permissão (método/moeda não habilitado, IP fora da allowlist). content: { application/problem+json: { schema: { $ref: "#/components/schemas/Problema" } } } NaoEncontrado: description: Objeto inexistente NESTA operação (objeto de outra operação também é 404). content: { application/problem+json: { schema: { $ref: "#/components/schemas/Problema" } } } Conflito: description: Idempotência violada, transição de estado inválida ou ETag divergente. content: { application/problem+json: { schema: { $ref: "#/components/schemas/Problema" } } } RegraDeNegocio: description: Recusa definitiva de regra (não repetir). content: { application/problem+json: { schema: { $ref: "#/components/schemas/Problema" } } } Limitado: description: Rate limit. `Retry-After` presente. headers: Retry-After: { schema: { type: integer } } content: { application/problem+json: { schema: { $ref: "#/components/schemas/Problema" } } } Indisponivel: description: Dracma/PSP indisponível. Repetir com a MESMA Idempotency-Key. headers: Retry-After: { schema: { type: integer } } content: { application/problem+json: { schema: { $ref: "#/components/schemas/Problema" } } } schemas: Money: type: object required: [amount, currency] additionalProperties: false properties: amount: type: string pattern: "^(0|[1-9][0-9]{0,37})$" description: Inteiro na menor unidade da moeda (sem sinal; o sentido vem do objeto). currency: type: string pattern: "^[A-Z0-9]{3,12}(\\.[A-Z0-9]{2,12})?$" description: ISO 4217 (BRL, USD, EUR…) ou ativo cripto com rede (USDT.TRON, USDC.ETH, BTC.BTC). Moeda: type: object required: [code, kind, exponent, methods] properties: code: { type: string, examples: [BRL, USDT.TRON] } kind: { type: string, enum: [fiat, crypto] } exponent: { type: integer, minimum: 0, maximum: 18, description: "casas da menor unidade (BRL=2, BTC=8, ETH=18)" } methods: type: array items: { $ref: "#/components/schemas/Metodo" } rails: type: array description: v1.1.0 — trilhos bancários habilitados para a moeda nesta operação, com prazos e cut-off. items: { $ref: "#/components/schemas/TrilhoDisponivel" } method_details: type: array description: | ADITIVO (dracma_api) — por método habilitado, o que o PSP de cash-in da operação exige no `POST /deposits` (`deposit_requirements`, caminhos de campo do corpo) e avisos de comportamento (`psp_notes`). Campo exigido e ausente → 400 `validation_failed` com `errors[].reason = required_by_operation`. `methods` continua sendo a lista de métodos. items: { $ref: "#/components/schemas/DetalheMetodo" } DetalheMetodo: type: object required: [method, deposit_requirements, psp_notes] properties: method: { $ref: "#/components/schemas/Metodo" } deposit_requirements: type: array description: Caminhos de campo exigidos no `CriarDeposito` (lista aberta; hoje `player.name`, `player.email`). items: { type: string, examples: [player.name, player.email] } psp_notes: type: array description: Avisos de comportamento do PSP (lista aberta; hoje `payer_identity_unavailable`). items: { type: string, examples: [payer_identity_unavailable] } Metodo: type: string enum: [pix, card, crypto, bank_transfer, boleto, wire, swift] description: | `pix` — PIX (BR). `card` — cartão, checkout hospedado. `crypto` — ativo com rede. `boleto` — boleto bancário (BR), só depósito. `bank_transfer` — v1.0, transferência bancária genérica; MANTIDO por compatibilidade e equivalente a `wire` com o trilho escolhido pelo Dracma. Integração nova DEVERIA usar `wire`/`swift`. `wire` (v1.1.0) — transferência bancária DOMÉSTICA/regional num trilho (`rail`): TED, ACH, Fedwire, SEPA, SEPA Instant, Faster Payments, SPEI… `swift` (v1.1.0) — transferência INTERNACIONAL pela rede SWIFT (MT103/pacs.008), com correspondentes, OUR/SHA/BEN e câmbio. Lista ABERTA: métodos novos só se acrescentam; o operador trata valor desconhecido como não suportado, nunca como erro fatal (CONTRATO.md §6.0). Tabela método × país × moeda × prazo em METODOS.md. Trilho: type: string pattern: "^[a-z][a-z0-9_]{1,31}$" description: | v1.1.0 — trilho (rail) de um `wire`. Lista ABERTA, registrada em METODOS.md (ex.: `ted`, `ach`, `ach_same_day`, `fedwire`, `sepa_ct`, `sepa_instant`, `faster_payments`, `bacs`, `chaps`, `spei`, `eft_ca`, `npp`, `imps`, `neft`). Para `swift` o trilho é `swift`. Trilho não habilitado → 400 `rail_not_enabled`. examples: [sepa_ct, ach, spei, faster_payments] TrilhoDisponivel: type: object required: [method, rail, directions, settlement] properties: method: { $ref: "#/components/schemas/Metodo" } rail: { $ref: "#/components/schemas/Trilho" } directions: { type: array, items: { type: string, enum: [in, out] } } settlement: type: object required: [min_days, max_days, business_days, calendar] properties: min_days: { type: integer, minimum: 0, maximum: 10, description: "D+n mínimo" } max_days: { type: integer, minimum: 0, maximum: 10, description: "D+n máximo (SWIFT típico D+1…D+5)" } business_days: { type: boolean, description: "true = D+n conta só dias úteis do calendário" } calendar: { type: string, examples: [BR, US-FED, TARGET2, GB, MX], description: "calendário de dias úteis (METODOS.md §3)" } cut_off_local: { type: [string, "null"], pattern: "^[0-2][0-9]:[0-5][0-9]$", description: "hora limite; depois dela, conta a partir do próximo dia útil. null = 24x7" } timezone: { type: [string, "null"], examples: [America/Sao_Paulo, America/New_York, Europe/Brussels] } limits: type: object properties: min: { $ref: "#/components/schemas/Money" } max: { $ref: "#/components/schemas/Money" } recall_window_days: { type: [integer, "null"], description: "janela em que um recall ainda pode ser pedido" } Endereco: type: object description: v1.1.0 — endereço postal (exigido para o beneficiário SWIFT; FATF R.16). required: [line1, city, country] additionalProperties: false properties: line1: { type: string, maxLength: 140 } line2: { type: string, maxLength: 140 } city: { type: string, maxLength: 70 } region: { type: string, maxLength: 70 } postal_code: { type: string, maxLength: 16 } country: { type: string, pattern: "^[A-Z]{2}$" } RoteamentoBancario: type: object description: | v1.1.0 — código de roteamento doméstico. Validação por `scheme` (REGRAS.md §17.1): `aba` 9 dígitos com dígito verificador 3-7-1; `sort_code` 6 dígitos; `bsb` 6 dígitos; `ifsc` `^[A-Z]{4}0[A-Z0-9]{6}$`; `transit` (CA) 5+3 dígitos; `ispb` 8 dígitos; `compe` 3 dígitos; `bank_code` genérico por país. required: [scheme, code] additionalProperties: false properties: scheme: { type: string, enum: [aba, sort_code, bsb, ifsc, transit, ispb, compe, bank_code] } code: { type: string, pattern: "^[A-Z0-9]{3,11}$" } branch: { type: string, maxLength: 16, description: "agência (BR) ou filial, quando o país exige" } BancoIntermediario: type: object description: v1.1.0 — banco intermediário/correspondente indicado pelo beneficiário (SWIFT). required: [bic] additionalProperties: false properties: bic: { type: string, pattern: "^[A-Z]{4}[A-Z]{2}[A-Z0-9]{2}([A-Z0-9]{3})?$" } name: { type: string, maxLength: 140 } country: { type: string, pattern: "^[A-Z]{2}$" } account: { type: string, maxLength: 34, description: "conta do banco do beneficiário no correspondente, se informada" } ContaBancaria: type: object description: | v1.1.0 — conta bancária de beneficiário (saque) ou de recebimento (instrução de depósito). Exatamente UMA forma de identificar a conta: `iban` OU (`account_number` + `routing`) OU `clabe`. Validação (REGRAS.md §17.1): IBAN com tamanho do país e mod-97 = 1 (ISO 13616); BIC 8 ou 11 com país coerente; CLABE 18 dígitos com dígito verificador; ABA com checksum. Falha → 400 `validation_failed` com `errors[].reason`. required: [holder_name, country] additionalProperties: false properties: holder_name: { type: string, maxLength: 140, description: "nome do titular como o banco o registra" } holder_type: { type: string, enum: [individual, business] } holder_address: { $ref: "#/components/schemas/Endereco" } holder_document: { $ref: "#/components/schemas/Documento" } country: { type: string, pattern: "^[A-Z]{2}$", description: "país da conta" } iban: { type: string, pattern: "^[A-Z]{2}[0-9]{2}[A-Z0-9]{11,30}$", description: "sem espaços, maiúsculas; checksum mod-97 conferido no servidor" } bic: { type: string, pattern: "^[A-Z]{4}[A-Z]{2}[A-Z0-9]{2}([A-Z0-9]{3})?$", description: "BIC/SWIFT de 8 ou 11" } account_number: { type: string, pattern: "^[A-Z0-9-]{4,34}$" } account_type: { type: string, enum: [checking, savings, payment] } routing: { $ref: "#/components/schemas/RoteamentoBancario" } clabe: { type: string, pattern: "^[0-9]{18}$", description: "México (SPEI)" } bank_name: { type: string, maxLength: 140 } bank_address: { $ref: "#/components/schemas/Endereco" } oneOf: - { required: [iban] } - { required: [account_number, routing] } - { required: [clabe] } DestinoWire: type: object description: v1.1.0 — destino de saque `wire` (doméstico/regional). required: [account] additionalProperties: false properties: rail: { $ref: "#/components/schemas/Trilho" } account: { $ref: "#/components/schemas/ContaBancaria" } DestinoSwift: type: object description: | v1.1.0 — destino de saque `swift`. `account.bic`, `account.holder_address` e `account.holder_name` são OBRIGATÓRIOS (sem eles → 400 `validation_failed`, reason `bic_required`/`address_required`). required: [account] additionalProperties: false properties: account: { $ref: "#/components/schemas/ContaBancaria" } intermediary_bank: { $ref: "#/components/schemas/BancoIntermediario" } charge_bearer: { $ref: "#/components/schemas/PortadorDeTarifa" } PortadorDeTarifa: type: string enum: [OUR, SHA, BEN] description: | v1.1.0 — quem paga as tarifas SWIFT (campo 71A): `OUR` a operação paga tudo (beneficiário recebe o valor cheio); `SHA` cada lado paga o seu banco e os correspondentes descontam do valor em trânsito; `BEN` tudo descontado do beneficiário. Ausente → default da operação nas regras (DECISOES D14). Proposito: type: object description: | v1.1.0 — propósito do pagamento, exigido em `swift` e em corredores que o pedem. `code` é canônico do Dracma; o Dracma o traduz para o código do corredor (ISO 20022 ExternalPurpose, natureza de câmbio do BCB etc.). required: [code] additionalProperties: false properties: code: { type: string, enum: [gaming_winnings, balance_withdrawal, deposit_refund, other] } description: { type: string, maxLength: 140 } Tarifa: type: object description: v1.1.0 — um item de tarifa. `bearer` diz de quem foi descontado. required: [type, amount, bearer] additionalProperties: false properties: type: { type: string, examples: [dracma, psp, correspondent, fx, return, recall], description: "lista aberta" } amount: { $ref: "#/components/schemas/Money" } bearer: { type: string, enum: [operator, beneficiary, payer] } estimated: { type: boolean, description: "true enquanto o correspondente não confirmou o valor" } Cambio: type: object description: | v1.1.0 — câmbio aplicado a um depósito/saque. `sell` sai do lado da operação, `buy` chega ao outro lado. `rate` = unidades de `buy` por 1 unidade MAIOR de `sell`, decimal em string (fórmula em MULTITENANT.md §6). required: [sell, buy, rate, risk_bearer] additionalProperties: false properties: quote_id: { type: [string, "null"], pattern: "^fxq_[A-Za-z0-9]+$" } sell: { $ref: "#/components/schemas/Money" } buy: { $ref: "#/components/schemas/Money" } rate: { type: string, pattern: "^[0-9]{1,12}(\\.[0-9]{1,12})?$" } mid_rate: { type: [string, "null"], pattern: "^[0-9]{1,12}(\\.[0-9]{1,12})?$" } spread_bps: { type: [integer, "null"], minimum: 0, maximum: 10000 } locked: { type: boolean, description: "true = taxa travada (cotação aceita); false = indicativa, fixada no envio" } risk_bearer: { type: string, enum: [dracma, operator, beneficiary], description: "quem assume a variação entre a cotação e a liquidação (DECISOES D15)" } quoted_at: { type: [string, "null"], format: date-time } expires_at: { type: [string, "null"], format: date-time } executed_at: { type: [string, "null"], format: date-time } PedidoCotacao: type: object required: [purpose, sell_currency, buy_currency] additionalProperties: false description: Exatamente um de `sell_amount` (valor na moeda da carteira) ou `buy_amount`. properties: purpose: { type: string, enum: [deposit, withdrawal] } method: { $ref: "#/components/schemas/Metodo" } sell_currency: { type: string } buy_currency: { type: string } sell_amount: { type: string, pattern: "^(0|[1-9][0-9]{0,37})$" } buy_amount: { type: string, pattern: "^(0|[1-9][0-9]{0,37})$" } oneOf: - { required: [sell_amount] } - { required: [buy_amount] } Cotacao: type: object required: [id, status, sell, buy, rate, expires_at, risk_bearer] properties: id: { type: string, examples: [fxq_01J9ZQ7] } status: { type: string, enum: [active, used, expired] } sell: { $ref: "#/components/schemas/Money" } buy: { $ref: "#/components/schemas/Money" } rate: { type: string } mid_rate: { type: string } spread_bps: { type: integer } risk_bearer: { type: string, enum: [dracma, operator, beneficiary] } quoted_at: { type: string, format: date-time } expires_at: { type: string, format: date-time } Compliance: type: object description: | v1.1.0 — estado do screening de sanções/PLD do objeto. NUNCA traz a lista, o nome casado nem o motivo detalhado (tipping-off, SEGURANCA.md §13.1). required: [status] additionalProperties: false properties: status: { type: string, enum: [not_required, pending, information_requested, cleared, rejected, frozen] } checked_at: { type: [string, "null"], format: date-time } reference: { type: [string, "null"], description: "id opaco do caso de compliance no Dracma" } documents_requested: type: array description: tipos de documento pedidos (ex. source_of_funds, proof_of_address, invoice) items: { type: string } DevolucaoBancaria: type: object description: v1.1.0 — dados de uma transferência devolvida pelo banco (return). required: [reason_code, returned_at] additionalProperties: false properties: reason_code: { type: string, description: "código do trilho (ISO 20022 ExternalReturnReason, ex. AC01, AC04, AM05, BE04; ACH R01…)" } reason: { type: string, enum: [invalid_account, closed_account, beneficiary_mismatch, regulatory, duplicate, recall, other], description: "categoria canônica (lista aberta)" } returned_amount: { $ref: "#/components/schemas/Money" } return_fee: { oneOf: [ { $ref: "#/components/schemas/Money" }, { type: "null" } ] } returned_at: { type: string, format: date-time } Recall: type: object description: v1.1.0 — pedido de recall de um saque enviado. required: [status, requested_at] additionalProperties: false properties: status: { type: string, enum: [requested, accepted, rejected] } reason_code: { type: string } requested_by: { type: string, enum: [operator, panel, dracma] } requested_at: { type: string, format: date-time } decided_at: { type: [string, "null"], format: date-time } RastreioBancario: type: object description: v1.1.0 — identificadores de rastreio no trilho. additionalProperties: false properties: uetr: { type: [string, "null"], format: uuid, description: "SWIFT gpi UETR" } rail_reference: { type: [string, "null"], description: "E2E id SEPA, IMAD/OMAD Fedwire, trace number ACH, clave de rastreo SPEI" } last_update_at: { type: [string, "null"], format: date-time } Documento: type: object required: [type, number, country] additionalProperties: false properties: type: { type: string, enum: [CPF, CNPJ, PASSPORT, NATIONAL_ID, TAX_ID] } number: { type: string, maxLength: 32, description: "só dígitos para CPF/CNPJ" } country: { type: string, pattern: "^[A-Z]{2}$" } Jogador: type: object required: [external_ref, document] additionalProperties: false properties: external_ref: type: string maxLength: 64 description: Identificador OPACO e imutável do jogador no operador (nunca e-mail/telefone). document: { $ref: "#/components/schemas/Documento" } name: { type: string, maxLength: 120 } email: { type: string, format: email, maxLength: 254 } birth_date: { type: string, format: date } kyc_level: { type: string, enum: [none, basic, full], description: "informativo" } ip: { type: string, description: "IP do jogador no pedido, para risco" } device_id: { type: string, maxLength: 128 } CriarDeposito: type: object required: [external_id, player, amount, method] additionalProperties: false properties: external_id: { type: string, maxLength: 64, description: "id do operador; único por operação" } player: { $ref: "#/components/schemas/Jogador" } amount: { $ref: "#/components/schemas/Money" } method: { $ref: "#/components/schemas/Metodo" } method_options: type: object additionalProperties: false properties: card: type: object properties: return_url: { type: string, format: uri } crypto: type: object properties: pay_currency: { type: string, description: "ativo em que o jogador paga (USDT.TRON); amount segue na moeda da carteira" } wire: type: object additionalProperties: false description: v1.1.0 properties: rail: { $ref: "#/components/schemas/Trilho" } reference_valid_days: { type: integer, minimum: 1, maximum: 30, default: 10, description: "dias corridos em que a referência casa; substitui expires_in_seconds para wire" } swift: type: object additionalProperties: false description: v1.1.0 properties: reference_valid_days: { type: integer, minimum: 1, maximum: 30, default: 15 } pay_currency: { type: string, description: "moeda em que o pagador envia, se ≠ da carteira (exige câmbio habilitado)" } boleto: type: object additionalProperties: false description: v1.1.0 properties: due_in_days: { type: integer, minimum: 1, maximum: 30, default: 3 } fx_quote_id: { type: string, pattern: "^fxq_[A-Za-z0-9]+$", description: "v1.1.0 — trava a cotação (pagamento em moeda ≠ da carteira)" } expires_in_seconds: { type: integer, minimum: 300, maximum: 86400, default: 1800 } locale: { type: string, examples: [pt-BR, en, es] } metadata: type: object maxProperties: 20 additionalProperties: { type: string, maxLength: 256 } InstrucoesDeposito: type: object additionalProperties: false properties: pix: type: object required: [copy_paste, expires_at] properties: copy_paste: { type: string, description: "BR Code EMV (payload do QR)" } qr_code_png_base64: { type: string, description: "opcional; o operador pode gerar o QR do copy_paste" } expires_at: { type: string, format: date-time } card: type: object required: [checkout_url, expires_at] properties: checkout_url: { type: string, format: uri, description: "página hospedada/hosted fields do Dracma; o PAN nunca passa pelo operador" } expires_at: { type: string, format: date-time } crypto: type: object required: [address, pay_amount, expires_at] properties: address: { type: string } memo: { type: string } pay_amount: { $ref: "#/components/schemas/Money" } rate: { type: string, description: "cotação travada, decimal em string" } expires_at: { type: string, format: date-time } wire: { $ref: "#/components/schemas/InstrucoesBancarias" } swift: { $ref: "#/components/schemas/InstrucoesBancarias" } boleto: type: object description: v1.1.0 required: [digitable_line, due_date] properties: digitable_line: { type: string, pattern: "^[0-9]{47,48}$" } barcode: { type: string, pattern: "^[0-9]{44}$" } pdf_url: { type: string, format: uri } due_date: { type: string, format: date } InstrucoesBancarias: type: object description: | v1.1.0 — como o pagador transfere. A `reference` é OBRIGATÓRIA na transferência (campo de remessa/"remittance information"/"concepto"): é o único elo entre o crédito e o depósito. Sem ela → `Recebimento` não casado. required: [account, reference, amount, expires_at] additionalProperties: false properties: rail: { $ref: "#/components/schemas/Trilho" } account: { $ref: "#/components/schemas/ContaBancaria" } intermediary_bank: { $ref: "#/components/schemas/BancoIntermediario" } reference: type: string pattern: "^RF[0-9]{2}[A-Z0-9]{1,14}$" description: "referência de credor ISO 11649 (RF + 2 dígitos de controle mod-97), ≤ 18 caracteres para caber no menor campo de remessa (Faster Payments)" amount: { $ref: "#/components/schemas/Money" } charge_bearer: { $ref: "#/components/schemas/PortadorDeTarifa" } expires_at: { type: string, format: date-time, description: "até quando a referência casa; depois disso o crédito vira Recebimento com EXPIRED_REFERENCE" } StatusDeposito: type: string enum: [created, pending, under_review, paid, expired, cancelled, failed, returned, refunded, chargeback, pending_compliance, frozen, paid_partial] description: | Máquina de estados em CONTRATO.md §6.1. Só `paid` credita (pelo `amount`). v1.1.0: `pending_compliance` (fundos recebidos, em screening — não credita), `frozen` (retido por sanção/ordem legal — não credita), `paid_partial` (terminal; aceito no painel valor MENOR que o cobrado — credita `paid_amount`, só operador v1.1; operador v1.0 trata como desconhecido e não credita). Deposito: type: object required: [id, external_id, status, version, amount, method, player_ref, created_at, updated_at] properties: id: { type: string, examples: [dep_01J9ZQ3] } external_id: { type: string } status: { $ref: "#/components/schemas/StatusDeposito" } version: { type: integer, minimum: 1, description: "incrementa a cada transição; ordena webhooks" } amount: { $ref: "#/components/schemas/Money" } paid_amount: { oneOf: [ { $ref: "#/components/schemas/Money" }, { type: "null" } ] } method: { $ref: "#/components/schemas/Metodo" } player_ref: { type: string } payer: type: [object, "null"] description: Quem de fato pagou (preenchido em paid/under_review). properties: document: { $ref: "#/components/schemas/Documento" } name: { type: string } matches_player: { type: boolean } instructions: { oneOf: [ { $ref: "#/components/schemas/InstrucoesDeposito" }, { type: "null" } ] } failure_code: { type: [string, "null"] } review_reasons: type: array description: "v1.0.1 — motivos de under_review (PAYER_MISMATCH, AMOUNT_MISMATCH, PAID_AFTER_EXPIRY). Vazio fora de revisão." items: { type: string } created_at: { type: string, format: date-time } updated_at: { type: string, format: date-time } paid_at: { type: [string, "null"], format: date-time } expires_at: { type: [string, "null"], format: date-time } rail: { oneOf: [ { $ref: "#/components/schemas/Trilho" }, { type: "null" } ], description: "v1.1.0" } received_amount: { oneOf: [ { $ref: "#/components/schemas/Money" }, { type: "null" } ], description: "v1.1.0 — total recebido do pagador (pode exceder `amount`; o excedente vira Recebimento a devolver)" } receipts: { type: array, items: { type: string }, description: "v1.1.0 — ids `rcv_` dos créditos bancários ligados a este depósito" } fee: { oneOf: [ { $ref: "#/components/schemas/Money" }, { type: "null" } ], description: "v1.1.0 — soma das tarifas com bearer=operator" } fees: { type: array, items: { $ref: "#/components/schemas/Tarifa" }, description: "v1.1.0" } fx: { oneOf: [ { $ref: "#/components/schemas/Cambio" }, { type: "null" } ], description: "v1.1.0" } compliance: { oneOf: [ { $ref: "#/components/schemas/Compliance" }, { type: "null" } ], description: "v1.1.0" } expected_settlement_date: { type: [string, "null"], format: date, description: "v1.1.0 — quando o valor fica disponível no caixa da operação (não é o crédito ao jogador)" } return: { oneOf: [ { $ref: "#/components/schemas/DevolucaoBancaria" }, { type: "null" } ], description: "v1.1.0 — em `returned` por trilho bancário" } CriarSaque: type: object required: [external_id, player, amount, method, destination] additionalProperties: false properties: external_id: { type: string, maxLength: 64 } player: { $ref: "#/components/schemas/Jogador" } amount: { $ref: "#/components/schemas/Money" } method: { $ref: "#/components/schemas/Metodo" } destination: type: object additionalProperties: false minProperties: 1 maxProperties: 1 description: "Exatamente UM método de destino, coerente com `method`." properties: pix: type: object required: [key_type, key] properties: key_type: { type: string, enum: [cpf, cnpj, email, phone, random] } key: { type: string, maxLength: 140 } crypto: type: object required: [address] properties: address: { type: string } memo: { type: string } bank_transfer: type: object description: v1.0 — forma livre, mantida por compatibilidade. Integração nova usa `wire`/`swift`. properties: country: { type: string } iban: { type: string } account: { type: object, additionalProperties: { type: string } } wire: { $ref: "#/components/schemas/DestinoWire" } swift: { $ref: "#/components/schemas/DestinoSwift" } purpose: { $ref: "#/components/schemas/Proposito" } fx_quote_id: { type: string, pattern: "^fxq_[A-Za-z0-9]+$", description: "v1.1.0 — cotação aceita; sem ela, câmbio é indicativo e fixado no envio (CONTRATO.md §6.5)" } payout_currency: { type: string, description: "v1.1.0 — moeda que o beneficiário recebe, se ≠ `amount.currency`. Exige câmbio habilitado; senão 422 `fx_not_enabled`." } review_requested: type: boolean default: false description: Pré-checagem do operador pediu revisão. Se true, o Dracma NÃO paga no automático. review_reasons: { type: array, items: { type: string } } player_context: type: object description: INFORMATIVO. Nunca usado para liberar automático. properties: total_deposited: { $ref: "#/components/schemas/Money" } total_withdrawn: { $ref: "#/components/schemas/Money" } account_created_at: { type: string, format: date-time } net_gaming_result: { type: string, description: "inteiro com sinal, menor unidade" } metadata: type: object maxProperties: 20 additionalProperties: { type: string, maxLength: 256 } StatusSaque: type: string enum: [requested, under_review, approved, processing, paid, rejected, failed, cancelled, returned, pending_compliance, sent, in_transit, recall_requested, recalled] description: | CONTRATO.md §6.2. Terminais com dinheiro de volta ao jogador — rejected, failed, cancelled, returned. Terminal pago — paid. v1.1.0 (não terminais, não movem dinheiro no operador): `pending_compliance`, `sent`, `in_transit`, `recall_requested`. Terminal v1.1.0: `recalled` (o dinheiro volta ao caixa da operação; o que a bet faz com o jogador é DECISOES D18). Decisao: type: object required: [mode, reasons, ruleset_version] properties: mode: { type: string, enum: [automatic, manual] } reasons: type: array description: Códigos de REGRAS.md (ex. AUTO_MULTIPLIER_EXCEEDED, FIRST_WITHDRAWAL, PIX_KEY_CHANGED). items: { type: string } ruleset_version: { type: integer } decided_by: { type: array, items: { type: string }, description: "ids opacos dos revisores (manual)" } decided_at: { type: [string, "null"], format: date-time } Saque: type: object required: [id, external_id, status, version, amount, method, player_ref, decision, created_at, updated_at] properties: id: { type: string, examples: [wdr_01J9ZQ4] } external_id: { type: string } status: { $ref: "#/components/schemas/StatusSaque" } version: { type: integer, minimum: 1 } amount: { $ref: "#/components/schemas/Money" } fee: { oneOf: [ { $ref: "#/components/schemas/Money" }, { type: "null" } ] } method: { $ref: "#/components/schemas/Metodo" } player_ref: { type: string } decision: { $ref: "#/components/schemas/Decisao" } psp_reference: { type: [string, "null"], description: "E2E id do PIX, hash da tx cripto; v1.1.0: UETR no SWIFT" } failure_code: { type: [string, "null"] } created_at: { type: string, format: date-time } updated_at: { type: string, format: date-time } paid_at: { type: [string, "null"], format: date-time } rail: { oneOf: [ { $ref: "#/components/schemas/Trilho" }, { type: "null" } ], description: "v1.1.0" } fees: { type: array, items: { $ref: "#/components/schemas/Tarifa" }, description: "v1.1.0 — detalhe; `fee` continua sendo a soma com bearer=operator" } charge_bearer: { oneOf: [ { $ref: "#/components/schemas/PortadorDeTarifa" }, { type: "null" } ], description: "v1.1.0" } fx: { oneOf: [ { $ref: "#/components/schemas/Cambio" }, { type: "null" } ], description: "v1.1.0" } purpose: { oneOf: [ { $ref: "#/components/schemas/Proposito" }, { type: "null" } ], description: "v1.1.0" } compliance: { oneOf: [ { $ref: "#/components/schemas/Compliance" }, { type: "null" } ], description: "v1.1.0" } expected_settlement_date: { type: [string, "null"], format: date, description: "v1.1.0 — data prevista de crédito ao beneficiário (calendário do trilho; recalculada a cada transição)" } sent_at: { type: [string, "null"], format: date-time, description: "v1.1.0" } tracking: { oneOf: [ { $ref: "#/components/schemas/RastreioBancario" }, { type: "null" } ], description: "v1.1.0" } return: { oneOf: [ { $ref: "#/components/schemas/DevolucaoBancaria" }, { type: "null" } ], description: "v1.1.0 — preenchido em `returned` por trilho bancário" } recall: { oneOf: [ { $ref: "#/components/schemas/Recall" }, { type: "null" } ], description: "v1.1.0" } StatusRecebimento: type: string enum: [unmatched, matched, returned, frozen] description: v1.1.0 — CONTRATO.md §6.4. Recebimento: type: object description: | v1.1.0 — crédito bancário recebido na conta da operação que NÃO casou automaticamente com um depósito. Não credita ninguém por si. Painel casa (`matched` → o depósito segue a própria máquina) ou devolve (`returned`). required: [id, status, version, amount, rail, reason, received_at, created_at, updated_at] properties: id: { type: string, examples: [rcv_01J9ZQ8] } status: { $ref: "#/components/schemas/StatusRecebimento" } version: { type: integer, minimum: 1 } amount: { $ref: "#/components/schemas/Money" } method: { $ref: "#/components/schemas/Metodo" } rail: { $ref: "#/components/schemas/Trilho" } reason: { type: string, enum: [NO_REFERENCE, UNKNOWN_REFERENCE, EXPIRED_REFERENCE, REFERENCE_ALREADY_PAID, EXCESS_AMOUNT, CURRENCY_MISMATCH], description: "lista aberta" } reference_provided: { type: [string, "null"], maxLength: 140 } payer: type: [object, "null"] description: dados do remetente como o banco informou (MASCARADOS na listagem) properties: name: { type: string } account_masked: { type: string } bic: { type: string } country: { type: string } matched_deposit_id: { type: [string, "null"] } return_by: { type: [string, "null"], format: date, description: "data-limite após a qual o Dracma devolve ao remetente (DECISOES D17)" } return: { oneOf: [ { $ref: "#/components/schemas/DevolucaoBancaria" }, { type: "null" } ] } received_at: { type: string, format: date-time } created_at: { type: string, format: date-time } updated_at: { type: string, format: date-time } LimitesJogador: type: object required: [player_ref, currency, total_deposited, total_withdrawn, auto_multiplier, auto_withdrawal_remaining] properties: player_ref: { type: string } currency: { type: string } total_deposited: { $ref: "#/components/schemas/Money" } total_withdrawn: { $ref: "#/components/schemas/Money" } auto_multiplier: { type: string, description: "X da regra, decimal em string (\"1\", \"1.5\")" } auto_withdrawal_remaining: { $ref: "#/components/schemas/Money" } blocked: { type: boolean } ConjuntoDeRegras: type: object description: Forma completa em REGRAS.md §10. Campos desconhecidos são rejeitados. required: [version, currencies] properties: version: { type: integer } timezone: { type: string, examples: [America/Sao_Paulo] } currencies: type: object additionalProperties: type: object properties: auto_multiplier: { type: string, default: "1" } deposit_min: { type: string } deposit_max: { type: string } withdrawal_min: { type: string } withdrawal_max: { type: string } auto_max_per_withdrawal: { type: string } auto_max_per_day: { type: string } dual_approval_above: { type: string } max_withdrawals_per_day: { type: integer } first_withdrawal_manual: { type: boolean, default: true } pix_key_change_cooldown_hours: { type: integer, default: 24 } require_payer_matches_player: { type: boolean, default: true } auto_hours: { type: [object, "null"], properties: { from: { type: string }, to: { type: string } } } bank_transfers: type: object description: v1.1.0 — regras de wire/SWIFT (REGRAS.md §17). Forma completa em REGRAS.md §10.1. properties: method_limits: type: object description: "por moeda → por método (`wire`, `swift`): withdrawal_min/max, deposit_min/max, auto_max_per_withdrawal (menor unidade, string). Vale o mais restritivo entre isto e `currencies.`." additionalProperties: type: object additionalProperties: type: object properties: deposit_min: { type: string } deposit_max: { type: string } withdrawal_min: { type: string } withdrawal_max: { type: string } auto_max_per_withdrawal: { type: [string, "null"] } swift_auto_enabled: { type: boolean, default: false, description: "false = todo saque SWIFT vai para revisão (SWIFT_MANUAL)" } default_charge_bearer: { $ref: "#/components/schemas/PortadorDeTarifa" } allowed_charge_bearers: { type: array, items: { $ref: "#/components/schemas/PortadorDeTarifa" } } beneficiary_change_cooldown_hours: { type: integer, default: 72 } screening_required_above: { type: [object, "null"], description: "por moeda: valor a partir do qual wire DOMÉSTICO também passa por pending_compliance (SWIFT sempre passa)", additionalProperties: { type: string } } blocked_countries: { type: array, items: { type: string, pattern: "^[A-Z]{2}$" }, description: "corredores vetados pela operação (somam-se aos vetados globalmente pelo Dracma)" } deposit_partial_accept_allowed: { type: boolean, default: false } deposit_excess_policy: { type: string, enum: [review, return_excess], default: review } unmatched_return_after_business_days: { type: integer, minimum: 1, maximum: 60, default: 10 } fx: type: object properties: enabled: { type: boolean, default: false } max_spread_bps: { type: integer, minimum: 0, maximum: 10000 } quote_ttl_seconds: { type: integer, minimum: 30, maximum: 86400, default: 900 } requote_tolerance_bps: { type: integer, minimum: 0, maximum: 10000, default: 50, description: "piora máxima de taxa aceita ao recotar uma cotação expirada no envio" } risk_bearer: { type: string, enum: [dracma, operator, beneficiary], default: beneficiary } Evento: type: object required: [id, type, created_at, operation_id, object_id, object_version, data] properties: id: { type: string, examples: [evt_01J9ZQ5] } type: type: string enum: - deposit.pending - deposit.under_review - deposit.paid - deposit.expired - deposit.cancelled - deposit.failed - deposit.returned - deposit.refunded - deposit.chargeback - withdrawal.under_review - withdrawal.approved - withdrawal.processing - withdrawal.paid - withdrawal.rejected - withdrawal.failed - withdrawal.cancelled - withdrawal.returned - rules.updated # --- v1.1.0 (aditivos; desconhecido → 200 sem mover dinheiro) --- - deposit.pending_compliance - deposit.frozen - deposit.paid_partial - withdrawal.pending_compliance - withdrawal.sent - withdrawal.in_transit - withdrawal.recall_requested - withdrawal.recall_rejected - withdrawal.recalled - receipt.unmatched - receipt.matched - receipt.returned - receipt.frozen created_at: { type: string, format: date-time } operation_id: { type: string } object_id: { type: string } object_version: { type: integer } data: type: object required: [object] properties: object: oneOf: - { $ref: "#/components/schemas/Deposito" } - { $ref: "#/components/schemas/Saque" } - { $ref: "#/components/schemas/ConjuntoDeRegras" } - { $ref: "#/components/schemas/Recebimento" } Reconciliacao: type: object required: [date, currency, items, next_cursor] properties: date: { type: string, format: date } currency: { type: string } items: type: array items: type: object required: [object_id, external_id, kind, status, amount, settled_at] properties: object_id: { type: string } external_id: { type: string } kind: type: string enum: [deposit, withdrawal, refund, chargeback, fee, return, recall, receipt] description: | v1.1.0 acrescenta `return` (wire/SWIFT devolvido ao caixa), `recall` (recall aceito) e `receipt` (recebimento não casado, em suspenso). Item de câmbio usa sempre o `amount` na moeda da carteira; a perna em outra moeda fica no objeto (`fx`). status: { type: string } amount: { $ref: "#/components/schemas/Money" } fee: { oneOf: [ { $ref: "#/components/schemas/Money" }, { type: "null" } ] } settled_at: { type: string, format: date-time } next_cursor: { type: [string, "null"] } totals: type: [object, "null"] properties: deposits_paid: { $ref: "#/components/schemas/Money" } withdrawals_paid: { $ref: "#/components/schemas/Money" } refunds: { $ref: "#/components/schemas/Money" } chargebacks: { $ref: "#/components/schemas/Money" } fees: { $ref: "#/components/schemas/Money" } count: { type: integer } digest: type: [string, "null"] description: SHA-256 hex da lista canônica do dia (CONTRATO.md §8), assinado no header `Dracma-Signature` da resposta. Problema: type: object required: [type, title, status, code] properties: type: { type: string, format: uri-reference } title: { type: string } status: { type: integer } code: type: string description: Catálogo em CONTRATO.md §7. detail: { type: string } trace_id: { type: string } retryable: { type: boolean } existing: type: object description: "v1.0.1 — em 409 duplicate_external_id, o objeto já existente (Deposito ou Saque)." errors: type: array items: type: object properties: field: { type: string } message: { type: string } reason: type: string description: "v1.1.0 — motivo máquina (lista aberta): iban_checksum, iban_length, bic_format, bic_country_mismatch, routing_checksum, clabe_checksum, account_invalid, bic_required, address_required, purpose_required, rail_mismatch. ADITIVO (dracma_api), emitidos hoje: required, format, enum, length, range, invalid, invalid_json, unknown, unknown_field, too_large, max_length, currency_mismatch, required_by_operation, reserved_domain_required" ```