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) e antes de rodar a suíte de conformidade.
Como funciona¶
0. Pré-requisitos¶
- Chave
key_test_…da operação Teste, com os escoposdeposits:write,withdrawals:writeeread, e o segredo num arquivo0400. - 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.
# 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?¶
dr GET /ping
Esperado: HTTP/… 200 e {"ok":true,"operation_id":"<uuid>","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¶
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:
{
"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.
DEP_EXT="dep_$(novo_id)"
cat > "$T"/dep.json <<EOF
{"external_id":"$DEP_EXT","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"}
EOF
dr POST /deposits "$T"/dep.json "$DEP_EXT" | tee "$T"/dep-resp.json
DEP_ID=$(js '["id"]' < "$T"/dep-resp.json); echo "$DEP_ID"
Esperado: 201 com "status":"pending" e instructions.pix.copy_paste contendo SANDBOX. Pode vir 202 com "status":"created": espere alguns segundos e faça o passo 5.
Repita exatamente o mesmo comando (mesmo arquivo, mesma chave) para ver a idempotência:
dr POST /deposits "$T"/dep.json "$DEP_EXT"
Esperado: a mesma resposta, com o header Idempotent-Replayed: true. Nenhum depósito novo.
4. Pagar no Teste¶
dr POST "/sandbox/deposits/$DEP_ID/pay" "" "sbx_$(novo_id)"
Corpo vazio significa "pago pelo próprio jogador, valor exato". Para ver um caminho de revisão, pague com outro CPF de teste:
echo '{"payer_document":{"type":"CPF","number":"11144477735","country":"BR"}}' > "$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¶
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¶
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:
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.
WDR_EXT="wdr_$(novo_id)"
cat > "$T"/wdr.json <<EOF
{"external_id":"$WDR_EXT","player":{"external_ref":"jogador-0001","document":{"type":"CPF","number":"52998224725","country":"BR"},"name":"Jogador Teste","ip":"203.0.113.10"},"amount":{"amount":"2000","currency":"BRL"},"method":"pix","destination":{"pix":{"key_type":"cpf","key":"52998224725"}},"review_requested":false}
EOF
dr POST /withdrawals "$T"/wdr.json "$WDR_EXT" | tee "$T"/wdr-resp.json
WDR_ID=$(js '["id"]' < "$T"/wdr-resp.json)
js '["status"], v["decision"]["mode"], v["decision"]["reasons"]' < "$T"/wdr-resp.json
Esperado: 201 e ('under_review', 'manual', ['FIRST_WITHDRAWAL']). Aprove (isto simula o revisor do painel) e pague:
dr POST "/sandbox/withdrawals/$WDR_ID/approve" "" "sbx_$(novo_id)" # → approved
dr POST "/sandbox/withdrawals/$WDR_ID/pay" "" "sbx_$(novo_id)" # → 200 com o estado atual; o paid chega depois
sleep 5
dr GET "/withdrawals/$WDR_ID" | js '["status"], v["psp_reference"]'
Esperado: ('paid', '<identificador da operação PIX>'). 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:
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¶
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¶
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¶
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 e Saques. As demais receitas (revisão, devolução, expiração) estão em Receitas por caminho.
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 |