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.
- Assinam chamadas operador para Dracma (pedido canônico mais HMAC-SHA256, Autenticação).
- Verificam webhooks Dracma para operador (
t=, um ou maisv1, janela de 300 s, Requisição e assinatura). - Calculam o digest da reconciliação e verificam a assinatura dele (Reconciliação).
- Repetem chamadas com backoff, mesma chave e mesmos bytes.
- Leem erros
problem+json, inclusive respostas da borda sem corpo. - Percorrem listagens por cursor opaco.
- 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, comvetores.she comSO_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_canonicoeDracma-Signatureidê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
v1do 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-Timestampdiferente dot=é recusado;v1com hex inválido, curto ou vazio não confere e não gera exceção;- digest: linhas ordenadas por
object_ide, no empate (o mesmo objeto em dois itens, por exemplodepositerefund), porkind, 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)¶
- O path assinado inclui
/v1. Com base.../v1, o path dePOST /depositsé/v1/deposits. Os exemplos tiram o path da URL final (req.URL.RequestURI(),url.pathname + url.search,parse_url) e evitam o erro. - Query exatamente como enviada. Não reordene, não decodifique
%XXe não troque+por%20depois de assinar. Monte a URL uma vez e assine essa string. No curl, use--path-as-is. - 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.parseoujson_decode. - 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 umPOST. - Timestamp em segundos. Milissegundos (
Date.now()) caem em401 timestamp_out_of_window. - Relógio. A janela é de ±300 s. Use NTP (chrony com 3 ou mais fontes). Chame
GET /v1/pingperiodicamente:go run ./cmd/pingcomparaserver_timecom o relógio local e alerta com desvio acima de 5 s. - Repetição de
POST. Em5xx,429ou erro de rede, repita com a mesmaIdempotency-Key(e timestamp, nonce e assinatura novos). Nunca gere chave nova para "tentar de novo" umPOSTde dinheiro.4xxnão se repete (Idempotência e repetição). - Comparação em tempo constante. Os exemplos usam
hmac.Equal,crypto.timingSafeEqual(com checagem de tamanho) ehash_equals. Olib.shdo shell não tem comparação em tempo constante e passa o segredo como argumento doopenssl, que fica visível emps. Use o shell só para depurar. - A verificação da assinatura não basta. Ela cobre só os itens 1 a 3 do webhook. Depois:
Dracma-Event-Idigual aoiddo corpo (senão400), dedup porDracma-Event-Ide 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¤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¶
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 oopenapi-generatorpara gerar tipos e rotas. O gerador não conhece a assinatura HMAC: você precisa acrescentar o cabeçalhoDracma-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 oRetry-Afteré um deles: useDracmaHttp::chamar()com os helpers. - Node abaixo de 22.18. O TypeScript direto não roda. Use
tsxou compile; a lógica é a mesma. - Shell.
helpers.shprecisa dejqoupython3para ler JSON. Os temporários ficam numa pasta oculta ao lado do script e são apagados no fim.