Documentação da API

Autenticação HMAC passo a passo

Como assinar cada requisição com HMAC-SHA256 no seu servidor, com exemplos em Node.js e curl.

Toda chamada à API (exceto GET /openapi.json e GET /changelog) é assinada pelo seu servidor. A assinatura prova que a requisição vem do operador e não foi alterada no caminho.

Cabeçalhos obrigatórios

Cabeçalho Valor
X-Operator-Key a chave da credencial (bk_…)
X-Timestamp epoch em segundos; a API aceita ±5 minutos
X-Signature hex do HMAC-SHA256 da string canônica, com o secret (sk_…)
Idempotency-Key obrigatório nas operações que criam ou movimentam dinheiro (veja idempotência)

Passo a passo

  1. Monte a string canônica: método em maiúsculas, caminho exatamente como enviado (com /api/v1 e a query string), timestamp e o SHA-256 (hex) do corpo.
  2. Calcule o HMAC-SHA256 da string canônica com o seu secret e converta para hex.
  3. Envie os cabeçalhos acima.
string_canonica = METODO + "\n" + CAMINHO_COM_QUERY + "\n" + TIMESTAMP + "\n" + SHA256_HEX(CORPO)
X-Signature     = HMAC_SHA256_HEX(secret, string_canonica)

Em requisições GET o corpo é vazio: use o SHA-256 da string vazia. Assine exatamente os bytes enviados — se o seu cliente HTTP reformatar o JSON, a assinatura não confere.

Exemplo completo em Node.js

import { createHash, createHmac, randomUUID } from "node:crypto";

const BASE_URL = process.env.API_BASE_URL; // ex.: https://<host-do-seu-ambiente>
const OPERATOR_KEY = process.env.OPERATOR_KEY;
const OPERATOR_SECRET = process.env.OPERATOR_SECRET;

export async function callApi(method, pathWithQuery, payload) {
  const body = payload === undefined ? "" : JSON.stringify(payload);
  const timestamp = Math.floor(Date.now() / 1000).toString();
  const bodyHash = createHash("sha256").update(body).digest("hex");
  const canonical = [method, pathWithQuery, timestamp, bodyHash].join("\n");
  const signature = createHmac("sha256", OPERATOR_SECRET).update(canonical).digest("hex");

  const headers = {
    "Content-Type": "application/json",
    "X-Operator-Key": OPERATOR_KEY,
    "X-Timestamp": timestamp,
    "X-Signature": signature,
  };
  if (method === "POST") headers["Idempotency-Key"] = randomUUID();

  const res = await fetch(`${BASE_URL}${pathWithQuery}`, {
    method,
    headers,
    ...(body ? { body } : {}),
  });
  return { status: res.status, body: await res.json() };
}

// Valida a assinatura:
const me = await callApi("GET", "/api/v1/me");

// Cadastra um jogador:
const player = await callApi("POST", "/api/v1/players", {
  externalId: "jogador-123",
  name: "Ana Souza",
  email: "ana@exemplo.com",
  cpf: "52998224725",
  birthDate: "1990-05-20",
});

Exemplo com curl

BODY='{"externalId":"jogador-123","name":"Ana Souza","email":"ana@exemplo.com","cpf":"52998224725","birthDate":"1990-05-20"}'
PATH_Q=/api/v1/players
TS=$(date +%s)
BODY_HASH=$(printf '%s' "$BODY" | openssl dgst -sha256 -hex | sed 's/^.* //')
CANONICAL=$(printf 'POST\n%s\n%s\n%s' "$PATH_Q" "$TS" "$BODY_HASH")
SIG=$(printf '%s' "$CANONICAL" | openssl dgst -sha256 -hmac "$OPERATOR_SECRET" -hex | sed 's/^.* //')

curl -X POST "$API_BASE_URL$PATH_Q" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "X-Operator-Key: $OPERATOR_KEY" \
  -H "X-Timestamp: $TS" \
  -H "X-Signature: $SIG" \
  -d "$BODY"

Snippets por linguagem e vetor de teste

A seção abaixo traz o código de assinatura em várias linguagens e um vetor de teste: use-o para conferir a sua implementação antes da primeira chamada real. Para quem prefere não escrever código, a coleção Postman e o testador assinam por você.

Erros comuns

Código de erro Causa provável
INVALID_CREDENTIALS cabeçalhos ausentes, chave errada, revogada ou de outro ambiente
TIMESTAMP_OUT_OF_RANGE relógio do servidor fora de sincronia (use NTP)
INVALID_SIGNATURE corpo reformatado após assinar; caminho sem /api/v1 ou sem a query; secret errado
FORBIDDEN_IP o IP de saída do seu servidor não está na lista permitida do operador

sign.mjs

import { createHash, createHmac, timingSafeEqual } from "node:crypto";

/** Assina uma requisição: retorna o valor do cabeçalho X-Signature. */
export function signRequest(secret, method, pathWithQuery, timestamp, body = "") {
  const canonical = [
    method.toUpperCase(),
    pathWithQuery, // exatamente como enviado, com /api/v1 e a query string
    timestamp, // epoch em segundos (string)
    createHash("sha256").update(body).digest("hex"),
  ].join("\n");
  return createHmac("sha256", secret).update(canonical).digest("hex");
}

/** Verifica um webhook recebido (use o corpo BRUTO, antes de parsear o JSON). */
export function verifyWebhook(secret, timestamp, rawBody, signature) {
  const expected = createHmac("sha256", secret).update(`${timestamp}.${rawBody}`).digest("hex");
  const a = Buffer.from(expected);
  const b = Buffer.from(signature);
  return a.length === b.length && timingSafeEqual(a, b);
}

Vetor de teste

Se o seu código gerar exatamente estes valores para as entradas abaixo, a assinatura está correta. O vetor também está em test-vectors.json.

Segredo
sk_test_vetor_de_teste_nao_use_em_producao
Timestamp
1700000000
POST — caminho
/api/v1/players
POST — corpo
{"externalId":"jogador-123","name":"Ana Souza"}
POST — X-Signature esperada
a1b0134cecb5e9a4578bfde7e86c68242be0a34e5ad31e367e5b4969e167b9aa
GET — caminho
/api/v1/players?externalId=jogador-123
GET (corpo vazio) — X-Signature esperada
df457411534d491459a8f5fcf846dcb98f9b4b3dcf9a6f0bc87c49b65b63c846