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
- Monte a string canônica: método em maiúsculas, caminho exatamente como enviado (com
/api/v1e a query string), timestamp e o SHA-256 (hex) do corpo. - Calcule o HMAC-SHA256 da string canônica com o seu secret e converta para hex.
- 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);
}sign.py
import hashlib
import hmac
def sign_request(secret, method, path_with_query, timestamp, body=""):
"""Assina uma requisição: retorna o valor do cabeçalho X-Signature."""
canonical = "\n".join([
method.upper(),
path_with_query, # exatamente como enviado, com /api/v1 e a query string
timestamp, # epoch em segundos (string)
hashlib.sha256(body.encode()).hexdigest(),
])
return hmac.new(secret.encode(), canonical.encode(), hashlib.sha256).hexdigest()
def verify_webhook(secret, timestamp, raw_body, signature):
"""Verifica um webhook recebido (use o corpo BRUTO, antes de parsear o JSON)."""
expected = hmac.new(secret.encode(), f"{timestamp}.{raw_body}".encode(), hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature)sign.php
<?php
/** Assina uma requisição: retorna o valor do cabeçalho X-Signature. */
function sign_request(string $secret, string $method, string $pathWithQuery, string $timestamp, string $body = ""): string {
$canonical = implode("\n", [
strtoupper($method),
$pathWithQuery, // exatamente como enviado, com /api/v1 e a query string
$timestamp, // epoch em segundos (string)
hash("sha256", $body),
]);
return hash_hmac("sha256", $canonical, $secret);
}
/** Verifica um webhook recebido (use o corpo BRUTO: file_get_contents("php://input")). */
function verify_webhook(string $secret, string $timestamp, string $rawBody, string $signature): bool {
return hash_equals(hash_hmac("sha256", $timestamp . "." . $rawBody, $secret), $signature);
}sign.go
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"fmt"
"strings"
)
// signRequest assina uma requisição: retorna o valor do cabeçalho X-Signature.
func signRequest(secret, method, pathWithQuery, timestamp, body string) string {
sum := sha256.Sum256([]byte(body))
canonical := strings.ToUpper(method) + "\n" +
pathWithQuery + "\n" + // exatamente como enviado, com /api/v1 e a query string
timestamp + "\n" + // epoch em segundos (string)
hex.EncodeToString(sum[:])
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(canonical))
return hex.EncodeToString(mac.Sum(nil))
}
// verifyWebhook verifica um webhook recebido (use o corpo BRUTO).
func verifyWebhook(secret, timestamp, rawBody, signature string) bool {
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(timestamp + "." + rawBody))
return hmac.Equal([]byte(hex.EncodeToString(mac.Sum(nil))), []byte(signature))
}BetSigner.java
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.HexFormat;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
public class BetSigner {
static String hmacHex(String secret, String message) throws Exception {
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
return HexFormat.of().formatHex(mac.doFinal(message.getBytes(StandardCharsets.UTF_8)));
}
/** Assina uma requisição: retorna o valor do cabeçalho X-Signature. */
public static String signRequest(String secret, String method, String pathWithQuery, String timestamp, String body) throws Exception {
String bodyHash = HexFormat.of().formatHex(
MessageDigest.getInstance("SHA-256").digest(body.getBytes(StandardCharsets.UTF_8)));
// pathWithQuery: exatamente como enviado, com /api/v1 e a query string; timestamp: epoch em segundos
return hmacHex(secret, String.join("\n", method.toUpperCase(), pathWithQuery, timestamp, bodyHash));
}
/** Verifica um webhook recebido (use o corpo BRUTO). */
public static boolean verifyWebhook(String secret, String timestamp, String rawBody, String signature) throws Exception {
String expected = hmacHex(secret, timestamp + "." + rawBody);
return MessageDigest.isEqual(expected.getBytes(StandardCharsets.UTF_8), signature.getBytes(StandardCharsets.UTF_8));
}BetSigner.cs
using System;
using System.Security.Cryptography;
using System.Text;
public static class BetSigner
{
static string HmacHex(string secret, string message)
{
using var h = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
return Convert.ToHexString(h.ComputeHash(Encoding.UTF8.GetBytes(message))).ToLowerInvariant();
}
/// <summary>Assina uma requisição: retorna o valor do cabeçalho X-Signature.</summary>
public static string SignRequest(string secret, string method, string pathWithQuery, string timestamp, string body = "")
{
var bodyHash = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(body))).ToLowerInvariant();
// pathWithQuery: exatamente como enviado, com /api/v1 e a query string; timestamp: epoch em segundos
return HmacHex(secret, string.Join("\n", method.ToUpperInvariant(), pathWithQuery, timestamp, bodyHash));
}
/// <summary>Verifica um webhook recebido (use o corpo BRUTO).</summary>
public static bool VerifyWebhook(string secret, string timestamp, string rawBody, string signature) =>
CryptographicOperations.FixedTimeEquals(
Encoding.UTF8.GetBytes(HmacHex(secret, $"{timestamp}.{rawBody}")),
Encoding.UTF8.GetBytes(signature));sign.sh
#!/usr/bin/env bash
# Assina uma requisição: imprime o valor do cabeçalho X-Signature.
# uso: sign_request SECRET METODO CAMINHO_COM_QUERY TIMESTAMP [CORPO]
sign_request() {
local secret="$1" method="$2" path="$3" ts="$4" body="${5:-}"
local body_hash
body_hash=$(printf '%s' "$body" | openssl dgst -sha256 -hex | sed 's/^.* //')
printf '%s\n%s\n%s\n%s' "$method" "$path" "$ts" "$body_hash" \
| openssl dgst -sha256 -hmac "$secret" -hex | sed 's/^.* //'
}
# Assinatura de um webhook: HMAC-SHA256 de "TIMESTAMP.CORPO_BRUTO" com o segredo whsec_...
webhook_signature() {
printf '%s.%s' "$2" "$3" | openssl dgst -sha256 -hmac "$1" -hex | sed 's/^.* //'
}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