Sandbox e conformidade¶
Estado: as rotas
/sandbox/*,/deposits,/withdrawalse/eventsestão implementadas (conferido no código em 2026-09-27), assim como a separaçãokey_test_/key_live_. Esta página segue o Contrato v1 1.1.0 (§9 e §10) e diz onde o código difere (divergencias.md).
1. O que é o sandbox¶
| Sandbox | Produção | |
|---|---|---|
| Base URL | https://sandbox.dracma.example/v1 |
https://api.dracma.example/v1 |
| Chave | key_test_… |
key_live_… |
| Dinheiro | Nenhum se move. Não há chamada a PSP real. | Real |
| mTLS | CA de sandbox | Obrigatório, com uma CA distinta |
Rotas /sandbox/* |
Existem | Respondem 404 |
- Nunca move dinheiro real. O PIX devolvido é um BR Code sintético: a chave do recebedor é
fictícia (
sandbox@dracma.invalid) e o campo de informação adicional traz o marcadorSANDBOX. Nenhum banco aceita esse código. Se umcopy_pastesemSANDBOXaparecer no seu ambiente de teste, pare e reporte: você está apontando para produção. - Cada ambiente recusa a chave do outro. Uma
key_test_em produção (ou umakey_live_no sandbox) recebe 401unknown_key. Isso já está implementado e evita o erro humano de apontar hml para prd. O formato do id ékey_(test|live)_[A-Za-z0-9]{1,64}. - Webhook só para URLs cadastradas na operação de sandbox. O Dracma nunca manda para uma URL que venha no corpo de uma requisição.
- Mesma autenticação de produção em todas as rotas, inclusive
/sandbox/*: HMAC §3, nonce e janela (ver autenticacao.md). - Os dados de teste usam só domínios reservados (
jogador1@operador.test) e CPFs de teste gerados (dígito verificador válido, sem titular real). Nunca use o e-mail ou o CPF de uma pessoa real, nem o seu. - O sandbox tem limites de taxa próprios, menores que os de produção. A suíte de conformidade tem um teto de objetos por execução.
2. Idempotency-Key também nas rotas /sandbox/*¶
Mande Idempotency-Key em TODO POST, inclusive nas rotas /sandbox/*. Use uma chave nova a
cada ação de controle, por exemplo sbx_<uuidv7>.
- A API exige a chave em todo
POSTe responde400 validation_failed(field: Idempotency-Key) quando ela falta. A suíte de conformidade também manda chave em todoPOSTdesde 2026-09-27. Oopenapi.yamlpublicado (o que a API serve) declara o header nessas rotas; o contrato 1.1.0 não declarava (referencia/openapi.md). - Use uma chave nova a cada vez: repetir a mesma chave numa ação de sandbox devolve a resposta guardada
(
Idempotent-Replayed: true) sem executar a ação de novo. Isso é o que você quer emPOST /deposits, mas confunde quando você quer, por exemplo, ligar e desligar/sandbox/outage.
3. Escopos das rotas de sandbox¶
| Rota | Escopo exigido |
|---|---|
POST /sandbox/deposits/{id}/pay, POST /sandbox/deposits/{id}/expire, POST /sandbox/deposits/{id}/refund, POST /sandbox/deposits/{id}/chargeback |
deposits:write |
POST /sandbox/withdrawals/{id}/{action} |
withdrawals:write |
POST /sandbox/outage, POST /sandbox/webhooks/redeliver |
deposits:write ou withdrawals:write |
GET /sandbox/webhooks |
read |
Sem o escopo exigido a resposta é 403 insufficient_scope. Um objeto de outra operação responde
404, nunca 403. Em produção, toda rota /sandbox/* responde 404 (a rota "não existe"), sem executar
nada.
4. Receitas: um caminho por vez¶
Em todas as receitas, POST significa: requisição assinada com Idempotency-Key. Espere o webhook ou
consulte o objeto (fetch-back) entre um passo e outro. Os corpos completos de criação estão em
depositos.md e saques.md.
4.1 Depósito pago pelo próprio jogador → paid¶
POST /v1/depositscomplayer.document= CPF do jogador. A resposta traz201compendingeinstructions.pix, ou202comcreated, que depois passa parapending.POST /v1/sandbox/deposits/{id}/pay(corpo opcional; vazio = pago pelo próprio jogador, valor exato):
A resposta é{"payer_document": {"type": "CPF", "number": "12345678909", "country": "BR"}}200com o objeto; opaidpode chegar logo depois, pelo webhook (o pagamento percorre o simulador do PSP). Só depending; outro estado →409 invalid_state_transition. Simulador fora →503.- Chega o webhook
deposit.paid. Faça o fetch-back comGET /v1/deposits/{id}e confirastatus: paid. Credite uma vez. - Confira que o evento aparece em
GET /v1/events?object_id={id}.
4.2 Pago por terceiro → under_review (PAYER_MISMATCH)¶
- No passo 2, mande um
payer_documentdiferente do documento do jogador, por exemplo outro CPF de teste. - Resultado:
under_review, comreview_reasonscontendoPAYER_MISMATCH. Nada é creditado. A decisão (devolver ou aprovar) é tomada no painel. Para testar a saída, aprove ou devolva pelo painel de sandbox e confira que sópaidcredita.
4.3 Valor divergente → under_review (AMOUNT_MISMATCH)¶
- No passo 2, mande
"paid_amount": {"amount": "4999", "currency": "BRL"}para um depósito de5000(moeda diferente da do depósito →400,reason: currency_mismatch). - Resultado:
under_reviewcomAMOUNT_MISMATCH. Nada é creditado. Na v1, a saída é devolver o valor.
4.4 Expiração → expired¶
- Crie o depósito e espere
pending. POST /v1/sandbox/deposits/{id}/expire→expired, com o webhookdeposit.expired.- Tentar pagar depois (
/pay) não leva apaid. POST /v1/deposits/{id}/cancel→ 409invalid_state_transition, porqueexpiredé terminal.
4.5 Saque automático → paid¶
- Prepare um jogador com um depósito
paid(receita 4.1). A regra X× conta só o que o Dracma liquidou. - Reserve o valor no seu livro e chame
POST /v1/withdrawalscom um valor dentro de X× ereview_requested: false. - A resposta traz
201comapprovededecision.mode = automatic(mandeplayer.ip: sem ele o saque nunca é automático —OWNERSHIP_UNVERIFIED). POST /v1/sandbox/withdrawals/{id}/pay(deapprovedouprocessing). A resposta é200com o objeto no estado atual; o desfecho chega depois:withdrawal.processingewithdrawal.paid→ fetch-back → finalize a reserva.
Atenção: por padrão, o primeiro saque de um jogador vai para revisão (
FIRST_WITHDRAWAL). Para testar o caminho automático, faça antes um primeiro saque e aprove-o com/approve.
4.6 Saque acima de X× → under_review (AUTO_MULTIPLIER_EXCEEDED)¶
- Peça um saque maior que X vezes o total depositado e liquidado.
- Resultado:
under_review,decision.mode = manualedecision.reasonscontendoAUTO_MULTIPLIER_EXCEEDED. A reserva continua.
4.7 review_requested: true nunca é automático¶
- Repita a receita 4.5 com
"review_requested": true. - Resultado:
under_review, mesmo que as regras liberassem o automático. O operador só pode tornar a decisão mais restritiva.
4.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 → approved (e o Dracma já despacha) |
Nada. A reserva continua. |
reject |
requested/under_review → rejected |
Devolve a reserva. |
pay |
approved/processing → (assíncrono) processing → paid |
Finaliza a saída quando ler paid. |
fail |
approved/processing → (assíncrono) failed |
Devolve a reserva quando ler failed. |
return |
paid → returned (síncrono) |
A política é sua. Recomendação: nada automático; abra uma decisão manual e dispare um alerta. |
pay e fail respondem 200 com o objeto no estado atual: o desfecho chega por withdrawal.*. Uma ação
fora da máquina de estados responde 409 invalid_state_transition. As ações compliance_hold,
compliance_clear, compliance_reject, send, transit, recall_accept e recall_reject são de wire ou
SWIFT: em PIX respondem 409. Ação fora do enum → 400 validation_failed. Simulador fora → 503 com
Retry-After: 5.
Cancelamento pelo operador: POST /v1/withdrawals/{id}/cancel. Funciona em requested, under_review e em
approved antes de o comando de envio sair; depois disso, 409 (saques.md).
4.9 Queda (outage) e retomada sem duplicar¶
POST /v1/sandbox/outagecom{"down": true}(resposta{"down": true}). A partir daí, toda rota da sua operação fora de/sandbox/*responde 503 (service_unavailable), comretryable: trueeRetry-After: 5.GET /v1/pingcontinua respondendo. Outras operações não são afetadas.POST /v1/depositscomIdempotency-Key: K→ 503.POST /v1/sandbox/outagecom{"down": false}, usando uma chave nova.- Repita o passo 2 com a mesma chave
Ke o mesmo corpo → 201 ou 202. Repita mais uma vez → a mesma resposta, agora comIdempotent-Replayed: true. 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 (ver erros-e-idempotencia.md).
4.10 Reentrega de webhook → testa o seu dedup¶
- Faça uma receita que gere um webhook (por exemplo a 4.1) e anote o
Dracma-Event-Id. POST /v1/sandbox/webhooks/redelivercom{"event_id": "evt_…"}→{"event_id": "evt_…", "scheduled": <n>}. Evento de outra operação →404;event_idinválido →400.- O seu receptor recebe o mesmo
Dracma-Event-Idde novo e responde 200 sem efeito. O crédito continua sendo 1. GET /v1/sandbox/webhooks→ log das entregas: evento, tentativa e o status HTTP que você devolveu. Use esse log para ver as suas respostas 401 e 5xx.
4.11 Webhook forjado não cria dinheiro¶
Assine você mesmo um deposit.paid com o seu segredo de webhook de sandbox, apontando para um depósito
que não está pago, e envie para o seu receptor. A assinatura é válida, mas o fetch-back devolve
pending. Resultado esperado: 200 e nenhum crédito (ver webhooks.md §3).
4.12 Estorno e chargeback depois de paid → refunded / chargeback¶
- Tenha um depósito
paid(receita 4.1) já creditado no seu livro. POST /v1/sandbox/deposits/{id}/refund(estorno) ouPOST /v1/sandbox/deposits/{id}/chargeback(MED / chargeback), corpo vazio ou{}. A resposta é200com o objeto emrefunded/chargebackeversionmaior; fora depaid→ 409invalid_state_transition; campo no corpo →400.- Chega o webhook
deposit.refundedoudeposit.chargeback. Faça o fetch-back e aplique a sua política de débito (recomendação:needs_decision+ alerta, nunca débito automático). - Essas duas rotas são aditivas ao openapi do contrato e só existem no sandbox; em produção ainda não há gatilho (divergencias.md, D-D3).
5. Suíte de conformidade (Contrato §10)¶
A suíte de conformidade é o teste de aceite do contrato. É um programa em Go, caixa-preta: fala só
HTTP e roda contra a sua operação de sandbox. Ela traz um operador de referência que recebe os
webhooks, verifica, deduplica, faz fetch-back e credita. Verde é pré-requisito do go-live (ver
go-live-operador.md). A suíte é entregue pelo Dracma no onboarding, junto com
as credenciais de sandbox. No repositório do Dracma ela mora em dracma_api/conformance/ (módulo Go próprio;
roda com cd conformance && go test ./conformance …). O contrato ainda cita o caminho antigo
nero_v2/nero_dracma_sandbox/conformidade/ (D-S2 em divergencias.md).
O que ela prova (casos obrigatórios v1.0, PIX)¶
Cada caso já foi visto falhar quando a regra correspondente é quebrada.
- Assinatura inválida, timestamp fora da janela e nonce repetido são recusados (401 com o
codecerto). - Idempotência: a mesma chave com o mesmo corpo devolve a mesma resposta; com corpo diferente, 409
idempotency_key_reused. - Depósito →
pay→ webhookdeposit.paidassinado, com fetch-back coerente, gera exatamente 1 crédito. - Um webhook reentregue com o mesmo
evtresponde 200 sem efeito. - Expiração:
expiredé terminal (pagar depois não virapaid; cancelar dá 409). - Saque dentro de X× →
approved/processing→paid. - Saque acima de X× →
under_reviewcomAUTO_MULTIPLIER_EXCEEDED. review_requested=truenunca é automático.- Objeto de outra operação → 404, em todas as rotas.
/sandbox/outage→ 503 retryable, e a retomada com a mesma chave não duplica.
Além desses, com as flags opcionais, ela cobre testes de segurança: chave de produção contra sandbox, allowlist de IP, revogação em até 5 min e detecção nos logs.
Como rodar¶
cd conformance && go test ./conformance -count=1 -v \
-dracma-url=https://sandbox.dracma.example/v1 \
-key-id=key_test_SUAOPERACAO \
-key-secret-file=/caminho/seguro/segredo-chamada \
-webhook-secret-file=/caminho/seguro/segredos-webhook \
-webhook-listen=127.0.0.1:0 \
-webhook-url=https://pagamentos.operador.example/dracma/webhook \
-currency=BRL
| Flag | Conteúdo |
|---|---|
-dracma-url |
Base do sandbox, incluindo /v1. |
-key-id / -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. O padrão é 127.0.0.1:0. |
-webhook-url |
URL cadastrada na operação que chega nesse listener. O padrão é http://<listen>/webhook. |
-currency |
Moeda da carteira. O padrão é BRL. |
-wait / -webhook-wait |
Espera máxima por uma transição (padrão 30 s) e por um webhook (padrão 90 s). |
| Opcionais | -key2-id/-key2-secret-file (operação de outro tenant, para o isolamento), -live-url/-live-key-id/-live-key-secret-file (recusa cruzada de ambiente), -blocked-ip-key-id/-blocked-ip-key-secret-file (allowlist), -revoke-key-id/-revoke-key-secret-file/-revoke-cmd/-revoke-deadline (revogação), -log-file (detecção), -openapi. |
Os arquivos de segredo ficam fora do repositório, com permissão 0400, entregues pelo seu cofre.
A suíte testa o Dracma usando um operador de referência. Ela não testa o seu conector. Para provar
o seu lado, rode as receitas da seção 4 contra o seu receptor e o seu livro, e confira: 1 crédito
por depósito pago, 0 créditos com webhook forjado, reentrega sem efeito, reserva devolvida em
rejected/failed/cancelled.