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).
  2. Verificam webhooks Dracma para operador (t=, um ou mais v1, janela de 300 s, Requisição e assinatura).
  3. Calculam o digest da reconciliação e verificam a assinatura dele (Reconciliação).
  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, 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: 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/ 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/ 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/ 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/ 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)
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).
  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).

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).

Exemplos

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

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

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)

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

# 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 e Saques.

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

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 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 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.
  • 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