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 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.
# 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&currency=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

Veja também