Documentação da API

A API Betshield permite integrar monitoramento de risco de apostas diretamente na sua plataforma. Envie eventos de apostas e consulte scores de risco calculados por modelos de IA.

Base URL

https://app.thebetshield.com/api/v1

Todas as requisições usam JSON (Content-Type: application/json) e retornam JSON. Autenticação via header Authorization.

Quickstart

Integre a Betshield em 3 passos:

  1. Obtenha sua API Key no dashboard em Configurações > API Keys
  2. Envie eventos de aposta, depósito e saque via POST /ingest
  3. Consulte scores de risco via GET /scores
# 1. Envie um evento
curl -X POST https://app.thebetshield.com/api/v1/ingest \
  -H "Authorization: Bearer betshield_key_aBcDeFgH..." \
  -H "Content-Type: application/json" \
  -d '{"events": [{"bettor_sha256": "ebdca3085b677490512e0078a35b8c93f5e8292845bfcab2f1aca03621c2e4f6", "event_type": "bet_placed", "timestamp": "2026-04-08T10:00:00-03:00", "stake": 150.00}]}'

# 2. Consulte o score
curl https://app.thebetshield.com/api/v1/scores/ebdca3085b677490512e0078a35b8c93f5e8292845bfcab2f1aca03621c2e4f6 \
  -H "Authorization: Bearer betshield_key_aBcDeFgH..."
import requests

API_KEY = "betshield_key_aBcDeFgH..."
BASE = "https://app.thebetshield.com/api/v1"
headers = {"Authorization": f"Bearer {API_KEY}"}

# 1. Envie um evento
requests.post(f"{BASE}/ingest", headers=headers, json={
    "events": [{
        "bettor_sha256": "ebdca3085b677490512e0078a35b8c93f5e8292845bfcab2f1aca03621c2e4f6",
        "event_type": "bet_placed",
        "timestamp": "2026-04-08T10:00:00-03:00",
        "stake": 150.00
    }]
})

# 2. Consulte o score
score = requests.get(f"{BASE}/scores/ebdca3085b677490512e0078a35b8c93f5e8292845bfcab2f1aca03621c2e4f6", headers=headers)
print(score.json())
const API_KEY = "betshield_key_aBcDeFgH...";
const BASE = "https://app.thebetshield.com/api/v1";
const headers = { "Authorization": `Bearer ${API_KEY}` };

// 1. Envie um evento
await fetch(`${BASE}/ingest`, {
  method: "POST",
  headers: { ...headers, "Content-Type": "application/json" },
  body: JSON.stringify({
    events: [{
      bettor_sha256: "ebdca3085b677490512e0078a35b8c93f5e8292845bfcab2f1aca03621c2e4f6",
      event_type: "bet_placed",
      timestamp: "2026-04-08T10:00:00-03:00",
      stake: 150.00
    }]
  })
});

// 2. Consulte o score
const res = await fetch(`${BASE}/scores/ebdca3085b677490512e0078a35b8c93f5e8292845bfcab2f1aca03621c2e4f6`, { headers });
const score = await res.json();
using var client = new HttpClient();
client.DefaultRequestHeaders.Add("Authorization", "Bearer betshield_key_aBcDeFgH...");

var baseUrl = "https://app.thebetshield.com/api/v1";

// 1. Envie um evento
var payload = @"{""events"": [{""bettor_sha256"": ""ebdca3085b677490512e0078a35b8c93f5e8292845bfcab2f1aca03621c2e4f6"", ""event_type"": ""bet_placed"", ""timestamp"": ""2026-04-08T10:00:00-03:00"", ""stake"": 150.00}]}";
await client.PostAsync(
    $"{baseUrl}/ingest",
    new StringContent(payload, Encoding.UTF8, "application/json")
);

// 2. Consulte o score
var scoreResp = await client.GetAsync($"{baseUrl}/scores/ebdca3085b677490512e0078a35b8c93f5e8292845bfcab2f1aca03621c2e4f6");
var score = await scoreResp.Content.ReadAsStringAsync();

Para um guia completo de integração, veja as seções Autenticação, Enviar Eventos e Consultar Scores.

O score não é imediato. O evento entra na hora, e a primeira nota pode levar até 24 horas depois do início do envio, então uma consulta feita antes disso responde 404.

Autenticação

Existem dois métodos de autenticação. Use API Key para integração server-to-server e JWT para acesso ao dashboard.

API Key (Bearer Token)

Inclua sua API Key no header de cada requisição:

Authorization: Bearer betshield_key_aBcDeFgH1234567890

POST /auth/login

POST /auth/login

Autentica um usuário com email e senha, retornando tokens de acesso e refresh.

CampoTipoObrigatórioDescrição
emailstringSimEmail do usuário
passwordstringSimSenha do usuário
curl -X POST https://app.thebetshield.com/api/v1/auth/login \
  -H "Content-Type: application/json" \
  -d '{
    "email": "joao@bet.com",
    "password": "s3nh@Segur4!"
  }'
import requests

response = requests.post(
    "https://app.thebetshield.com/api/v1/auth/login",
    json={
        "email": "joao@bet.com",
        "password": "s3nh@Segur4!"
    }
)

print(response.json())
const response = await fetch(
  "https://app.thebetshield.com/api/v1/auth/login",
  {
    method: "POST",
    headers: {
      "Content-Type": "application/json"
    },
    body: JSON.stringify({
      email: "joao@bet.com",
      password: "s3nh@Segur4!"
    })
  }
);

const data = await response.json();
using var client = new HttpClient();

var payload = @"{
  ""email"": ""joao@bet.com"",
  ""password"": ""s3nh@Segur4!""
}";

var content = new StringContent(
    payload, Encoding.UTF8, "application/json"
);

var response = await client.PostAsync(
    "https://app.thebetshield.com/api/v1/auth/login", content
);

var json = await response.Content.ReadAsStringAsync();
// Resposta 200 OK
{
  "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyXzEyMzQ1...",
  "refresh_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJyZWZyZXNoXzk4Nz...",
  "token_type": "bearer",
  "expires_in": 3600
}

POST /auth/refresh

POST /auth/refresh

Renova o token de acesso usando o refresh token.

CampoTipoObrigatórioDescrição
refresh_tokenstringSimRefresh token obtido no login
curl -X POST https://app.thebetshield.com/api/v1/auth/refresh \
  -H "Content-Type: application/json" \
  -d '{
    "refresh_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJyZWZyZXNoXzk4Nz..."
  }'
import requests

response = requests.post(
    "https://app.thebetshield.com/api/v1/auth/refresh",
    json={
        "refresh_token": "eyJhbGciOiJSUzI1NiIs..."
    }
)

print(response.json())
const response = await fetch(
  "https://app.thebetshield.com/api/v1/auth/refresh",
  {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      refresh_token: "eyJhbGciOiJSUzI1NiIs..."
    })
  }
);

const data = await response.json();
using var client = new HttpClient();

var payload = @"{
  ""refresh_token"": ""eyJhbGciOiJSUzI1NiIs...""
}";

var content = new StringContent(
    payload, Encoding.UTF8, "application/json"
);

var response = await client.PostAsync(
    "https://app.thebetshield.com/api/v1/auth/refresh", content
);

var json = await response.Content.ReadAsStringAsync();
// Resposta 200 OK
{
  "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyXzEyMzQ1...",
  "refresh_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJyZWZyZXNoXzU2Nz...",
  "token_type": "bearer",
  "expires_in": 3600
}

POST /auth/logout

POST /auth/logout

Revoga a sessão atual, invalidando o token de acesso e o refresh token.

curl -X POST https://app.thebetshield.com/api/v1/auth/logout \
  -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIs..."
import requests

response = requests.post(
    "https://app.thebetshield.com/api/v1/auth/logout",
    headers={"Authorization": "Bearer eyJhbGciOiJSUzI1NiIs..."}
)

print(response.status_code)  # 200
const response = await fetch(
  "https://app.thebetshield.com/api/v1/auth/logout",
  {
    method: "POST",
    headers: {
      "Authorization": "Bearer eyJhbGciOiJSUzI1NiIs..."
    }
  }
);
// response.status === 200
using var client = new HttpClient();
client.DefaultRequestHeaders.Add(
    "Authorization", "Bearer eyJhbGciOiJSUzI1NiIs..."
);

var response = await client.PostAsync(
    "https://app.thebetshield.com/api/v1/auth/logout", null
);
// response.StatusCode == HttpStatusCode.OK
// Resposta 200 OK
{
  "message": "Logged out successfully"
}

GET /auth/me

GET /auth/me

Retorna o perfil do usuário autenticado, incluindo email, nome, papel e organização.

curl https://app.thebetshield.com/api/v1/auth/me \
  -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIs..."
import requests

response = requests.get(
    "https://app.thebetshield.com/api/v1/auth/me",
    headers={"Authorization": "Bearer eyJhbGciOiJSUzI1NiIs..."}
)

print(response.json())
const response = await fetch(
  "https://app.thebetshield.com/api/v1/auth/me",
  {
    headers: {
      "Authorization": "Bearer eyJhbGciOiJSUzI1NiIs..."
    }
  }
);

const data = await response.json();
using var client = new HttpClient();
client.DefaultRequestHeaders.Add(
    "Authorization", "Bearer eyJhbGciOiJSUzI1NiIs..."
);

var response = await client.GetAsync(
    "https://app.thebetshield.com/api/v1/auth/me"
);

var json = await response.Content.ReadAsStringAsync();
// Resposta 200 OK
{
  "id": "1a7b3c95-2d48-4e17-8f60-9b5a4c2d7e83",
  "email": "joao@bet.com",
  "name": "João Silva",
  "role": "admin",
  "organization": "Minha Operadora",
  "created_at": "2026-01-15T10:30:00Z"
}

GET /auth/setup/{token}

GET /auth/setup/{token}

Valida um link de configuração de conta e retorna o email associado.

ParâmetroTipoLocalDescrição
tokenstringPathToken de setup recebido por email
curl https://app.thebetshield.com/api/v1/auth/setup/tok_xYz789AbCdEf012345
import requests

token = "tok_xYz789AbCdEf012345"
response = requests.get(
    f"https://app.thebetshield.com/api/v1/auth/setup/{token}"
)

print(response.json())
const token = "tok_xYz789AbCdEf012345";

const response = await fetch(
  `https://app.thebetshield.com/api/v1/auth/setup/${token}`
);

const data = await response.json();
using var client = new HttpClient();

var token = "tok_xYz789AbCdEf012345";
var response = await client.GetAsync(
    $"https://app.thebetshield.com/api/v1/auth/setup/{token}"
);

var json = await response.Content.ReadAsStringAsync();
// Resposta 200 OK
{
  "email": "maria@bet.com",
  "organization": "Minha Operadora",
  "expires_at": "2026-04-12T23:59:59Z"
}

POST /auth/setup/{token}

POST /auth/setup/{token}

Cria a conta do usuário convidado, definindo nome e senha, e retorna tokens de acesso.

CampoTipoObrigatórioDescrição
tokenstringSimToken de setup (path)
namestringSimNome completo do usuário
passwordstringSimSenha (mínimo 8 caracteres)
curl -X POST https://app.thebetshield.com/api/v1/auth/setup/tok_xYz789AbCdEf012345 \
  -H "Content-Type: application/json" \
  -d '{
    "email": "maria@bet.com",
    "name": "Maria Santos",
    "password": "s3nh@Segur4!"
  }'
import requests

token = "tok_xYz789AbCdEf012345"
response = requests.post(
    f"https://app.thebetshield.com/api/v1/auth/setup/{token}",
    json={
        "email": "maria@bet.com",
        "name": "Maria Santos",
        "password": "s3nh@Segur4!"
    }
)

print(response.json())
const token = "tok_xYz789AbCdEf012345";

const response = await fetch(
  `https://app.thebetshield.com/api/v1/auth/setup/${token}`,
  {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      email: "maria@bet.com",
      name: "Maria Santos",
      password: "s3nh@Segur4!"
    })
  }
);

const data = await response.json();
using var client = new HttpClient();

var token = "tok_xYz789AbCdEf012345";
var payload = @"{
  ""email"": ""maria@bet.com"",
  ""name"": ""Maria Santos"",
  ""password"": ""s3nh@Segur4!""
}";

var content = new StringContent(
    payload, Encoding.UTF8, "application/json"
);

var response = await client.PostAsync(
    $"https://app.thebetshield.com/api/v1/auth/setup/{token}", content
);

var json = await response.Content.ReadAsStringAsync();
// Resposta 200 OK
{
  "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyXzY3ODkw...",
  "refresh_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJyZWZyZXNoXzEyMz...",
  "token_type": "bearer",
  "expires_in": 3600,
  "user": {
    "id": "8c4d2e15-7a93-4f60-b1e8-5d3c9a7b2f41",
    "email": "maria@bet.com",
    "name": "Maria Santos",
    "role": "analyst"
  }
}

Enviar Eventos

POST /ingest

Envia um array de eventos de aposta para processamento de risco (máximo 2.000 por requisição).

Três campos obrigatórios

Só três informações são exigidas: quem é o apostador, o que aconteceu e quando aconteceu. Nenhum outro campo é exigido, e a ausência de um campo opcional nunca causa erro nem descarte.

Os nomes usados nesta página são os internos da Betshield e valem como exemplo. A sua marca é configurada para ler os nomes que os seus eventos já têm, inclusive os três obrigatórios, então não é preciso renomear nada.

{
  "bettor_sha256": "ebdca3085b677490512e0078a35b8c93f5e8292845bfcab2f1aca03621c2e4f6",
  "event_type": "bet_placed",
  "timestamp": "2026-04-08T10:00:00-03:00"
}
CampoTipoDescrição
bettor_sha256stringQuem é o apostador. O valor pode ser o CPF ou o SHA-256 dele, conforme a marca foi configurada. Veja Envio de CPF logo abaixo.
event_typestringO tipo do evento, com o nome que a sua plataforma já usa, seja deposito, dep, deposit_completed ou um código numérico. A tradução para o vocabulário interno da Betshield é configurada por marca no onboarding, e nenhum tipo é recusado por não ter equivalente.
timestampstringQuando o evento aconteceu, em ISO 8601 com fuso horário.

Se a sua marca usar um identificador próprio de evento, ele é configurado no onboarding e passa a ser exigido junto com os três acima, porque é ele que impede o mesmo evento de entrar duas vezes. Um evento sem esse identificador é recusado, e a resposta do lote nomeia o campo que faltou.

Os nomes dos campos são os seus

Cada marca tem a leitura dos próprios dados configurada no onboarding, e isso vale para todos os campos, os três obrigatórios inclusive. Se os eventos trazem documento, tipo e data_hora, a marca é configurada com esses nomes e o envio fica assim, sem nenhuma alteração no sistema de origem:

{
  "documento": "123.456.789-09",
  "tipo": "aposta_realizada",
  "data_hora": "2026-04-08T10:00:00-03:00",
  "valor": 50.00
}

O campo pode estar aninhado, e o endereço é configurado da mesma forma: apostador.documento funciona tanto quanto documento. O mesmo vale para os tipos de evento, que podem chegar em português, abreviados ou como códigos numéricos.

Envio de CPF

O CPF pode ser enviado diretamente. A Betshield converte para SHA-256 na entrada e o número em si nunca chega ao armazenamento, porque é removido do evento antes de gravar, inclusive quando vem dentro de um objeto aninhado. A pontuação é irrelevante: 123.456.789-09 e 12345678909 produzem o mesmo resultado.

Se a sua plataforma já trabalha com o hash, também é possível: a marca é configurada para receber o valor pronto e a Betshield passa a exigir os 64 caracteres hexadecimais, recusando o evento se um CPF cru aparecer nesse campo. Essas são as duas opções, e a escolha é feita uma vez, no onboarding.

Como calcular o hash

Só é necessário se a sua marca for configurada para enviar o identificador já convertido, ou se você quiser cruzar os apostadores do painel com a sua própria base. São quatro passos, e o resultado é determinístico: o mesmo CPF sempre produz o mesmo hash, em qualquer linguagem.

  1. Remova tudo que não for dígito. Ponto, hífen e espaço saem.
  2. Confira que sobraram exatamente 11 dígitos, com os zeros à esquerda preservados.
  3. Aplique SHA-256 sobre esses 11 dígitos, codificados em UTF-8.
  4. Escreva o resultado em hexadecimal, 64 caracteres.
# o -n e essencial: sem ele o sha256sum inclui a quebra de linha
printf '%s' "12345678909" | sha256sum
import hashlib, re

def hash_cpf(cpf):
    digitos = re.sub(r"\D", "", str(cpf))
    if len(digitos) != 11:
        raise ValueError(f"CPF invalido: {len(digitos)} digitos")
    return hashlib.sha256(digitos.encode("utf-8")).hexdigest()
const { createHash } = require("crypto");

function hashCpf(cpf) {
  const digitos = String(cpf).replace(/\D/g, "");
  if (digitos.length !== 11) throw new Error("CPF invalido");
  return createHash("sha256").update(digitos, "utf8").digest("hex");
}
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;

static string HashCpf(string cpf)
{
    var digitos = Regex.Replace(cpf, @"\D", "");
    if (digitos.Length != 11) throw new ArgumentException("CPF invalido");
    var bytes = SHA256.HashData(Encoding.UTF8.GetBytes(digitos));
    return Convert.ToHexString(bytes).ToLowerInvariant();
}

Como validar se deu certo

Rode a sua implementação sobre os CPFs abaixo e compare com o resultado esperado. Se os três baterem, a sua conversão está correta e não é preciso nos consultar. Estes são CPFs de teste, sem titular real.

CPF de entradaSHA-256 esperado
123.456.789-097ec94663084bd506d4f0c3e21042df233681fd7426e93f397c921b1d3e397bba
123456789097ec94663084bd506d4f0c3e21042df233681fd7426e93f397c921b1d3e397bba
012.345.678-90ee29eb4a8725678278ac439cf7abfd2a849cdc7378a6b6316017b81c51d720e7

As duas primeiras linhas são o mesmo CPF e produzem o mesmo hash, o que confirma que a pontuação foi removida. A terceira começa com zero e existe por um motivo, explicado abaixo.

O zero à esquerda é o erro mais comum

Um CPF guardado como número perde o zero inicial: 01234567890 vira 1234567890, com dez dígitos. Convertido assim, o hash tem os mesmos 64 caracteres hexadecimais de um hash válido, é aceito na ingestão e nunca corresponde ao apostador certo, sem erro nenhum em lugar algum. Leia o CPF sempre como texto, e confira os 11 dígitos antes de converter.

Enviando o CPF diretamente esse risco não existe: a Betshield conta os dígitos na entrada e recusa o evento na resposta da própria requisição, dizendo quantos dígitos encontrou.

Marca derivada da API key

A marca de cada evento é resolvida automaticamente a partir da API key utilizada. Não envie brand_slug nem brand_id no payload, porque esses campos são ignorados sem aviso. Para enviar eventos de marcas diferentes, use uma API key distinta para cada marca.

Qualquer outro campo é aceito

Fora dos três obrigatórios, o evento aceita qualquer campo. Não existe lista fechada, campo desconhecido não causa erro e nada é descartado por não estar previsto aqui. O evento é guardado inteiro, exatamente como foi enviado. A tabela abaixo é só um exemplo dos campos mais comuns.

CampoTipoO que é, se você tiver
stakenumberValor apostado, em reais
oddsnumberCotação da aposta
sportstringEsporte da aposta
in_playbooleanSe a aposta foi ao vivo
amountnumberValor do depósito ou do saque, em reais
methodstringMeio de pagamento
resultstringResultado da aposta liquidada
payoutnumberValor de retorno, em reais
bet_ref_idstringIdentificador da aposta na sua plataforma
limit_typestringQue limite foi alterado
limit_valuenumberO novo valor do limite
limit_directionstringSe o limite subiu ou desceu

O envelope

CampoTipoObrigatórioDescrição
eventsarraySimArray de eventos, no máximo 2.000 por requisição

Exemplos

curl -X POST https://app.thebetshield.com/api/v1/ingest \
  -H "Authorization: Bearer betshield_key_aBcDeFgH1234567890" \
  -H "Content-Type: application/json" \
  -d '{
    "events": [
      {
        "bettor_sha256": "a3f5e9b2c1d4e6f8a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3",
        "event_type": "bet_placed",
        "timestamp": "2026-04-05T14:30:00-03:00",
        "stake": 150.00,
        "odds": 2.15,
        "sport": "futebol",
        "in_play": true
      },
      {
        "bettor_sha256": "a3f5e9b2c1d4e6f8a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3",
        "event_type": "deposit",
        "timestamp": "2026-04-05T14:28:00-03:00",
        "stake": 500.00,
        "method": "pix"
      }
    ]
  }'
import requests

response = requests.post(
    "https://app.thebetshield.com/api/v1/ingest",
    headers={"Authorization": "Bearer betshield_key_aBcDeFgH1234567890"},
    json={
        "events": [
            {
                "bettor_sha256": "a3f5e9b2c1d4e6f8a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3",
                "event_type": "bet_placed",
                "timestamp": "2026-04-05T14:30:00-03:00",
                "stake": 500.00,
                "stake": 150.00, "odds": 2.15, "sport": "futebol"
            },
            {
                "bettor_sha256": "a3f5e9b2c1d4e6f8a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3",
                "event_type": "deposit",
                "timestamp": "2026-04-05T14:28:00-03:00",
                "stake": 1000.00
            }
        ]
    }
)

print(response.json())
const response = await fetch(
  "https://app.thebetshield.com/api/v1/ingest",
  {
    method: "POST",
    headers: {
      "Authorization": "Bearer betshield_key_aBcDeFgH1234567890",
      "Content-Type": "application/json"
    },
    body: JSON.stringify({
      events: [
        {
          bettor_sha256: "a3f5e9b2c1d4e6f8a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3",
          event_type: "bet_placed",
          timestamp: "2026-04-05T14:30:00-03:00",
          stake: 500.00,
          stake: 150.00, odds: 2.15, sport: "futebol"
        },
        {
          bettor_sha256: "a3f5e9b2c1d4e6f8a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3",
          event_type: "deposit",
          timestamp: "2026-04-05T14:28:00-03:00",
          stake: 1000.00
        }
      ]
    })
  }
);

const data = await response.json();
using var client = new HttpClient();
client.DefaultRequestHeaders.Add(
    "Authorization", "Bearer betshield_key_aBcDeFgH1234567890"
);

var payload = @"{
  ""events"": [
    {
      ""bettor_sha256"": ""a3f5e9b2c1d4e6f8a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3"",
      ""event_type"": ""bet_placed"",
      ""timestamp"": ""2026-04-05T14:30:00-03:00"",
      ""stake"": 500.00,
      ""stake"": 150.00, ""odds"": 2.15, ""sport"": ""futebol""
    },
    {
      ""bettor_sha256"": ""a3f5e9b2c1d4e6f8a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3"",
      ""event_type"": ""deposit"",
      ""timestamp"": ""2026-04-05T14:28:00-03:00"",
      ""stake"": 1000.00
    }
  ]
}";

var content = new StringContent(
    payload, Encoding.UTF8, "application/json"
);

var response = await client.PostAsync(
    "https://app.thebetshield.com/api/v1/ingest", content
);

var json = await response.Content.ReadAsStringAsync();

Resposta

// Resposta 200 OK
{
  "batch_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "total": 2,
  "accepted": 2,
  "skipped": 0,
  "rejected": 0,
  "skipped_reasons": {},
  "errors": []
}

Os três destinos de um evento

Cada evento do lote cai em um de três destinos, e a diferença entre eles decide o que o seu job faz.

accepted: o evento foi aceito e será processado.

skipped foi entendido e não gera score, e não há nada a corrigir. O caso comum é o evento sem identificador do apostador: um terminal físico de varejo, ou um cadastro ainda sem documento. O comportamento monitorado é sempre de uma pessoa, então um evento que não aponta para ninguém não tem como virar nota. O motivo vem agregado, e não item a item, porque num lote de dois mil o que orienta a ação é o volume e o motivo.

{
  "total": 2000,
  "accepted": 1830,
  "skipped": 170,
  "rejected": 0,
  "skipped_reasons": { "no_bettor_identifier": 170 },
  "errors": []
}

Uma proporção estável de skipped é normal e não pede ação. O que merece atenção é ela saltar de repente, ou chegar perto de 100%, o que indica configuração de leitura apontando para o campo errado.

rejected está malformado e precisa de correção na origem. Vem com o índice do evento no lote e o motivo. A contagem em rejected é sempre exata; a lista errors traz no máximo os 50 primeiros, porque um lote inteiro malformado devolveria milhares de mensagens quase iguais.

{
  "total": 3,
  "accepted": 2,
  "skipped": 0,
  "rejected": 1,
  "errors": [
    { "index": 2, "message": "Field 'created_at' must be an ISO 8601 timestamp with timezone..." }
  ]
}

Nomes de campo e estrutura

Você não precisa renomear nada. A leitura dos seus campos é configurada do lado da Betshield, uma vez por marca, a partir de uma amostra dos seus eventos. Vale para o identificador do apostador, para o tipo, para a data e para os campos de valor.

Campos aninhados são suportados. Se o identificador mora dentro de um objeto, o endereço é escrito com ponto, como player.document_hash, e o valor da aposta como data.amount. Um mesmo campo pode ter mais de um endereço quando ele muda de lugar conforme o tipo do evento.

O identificador do apostador

Quando a marca é configurada para receber o identificador já em hash, o valor precisa ser um SHA-256 escrito em 64 caracteres hexadecimais. Qualquer outra coisa é recusada na resposta da própria requisição, e o valor recusado não volta na mensagem de erro, porque um documento sem hash é dado pessoal. Maiúsculas e minúsculas são equivalentes.

Evento sem identificador nenhum não é erro: ele é contado como ignorado, com o motivo na resposta, e nada precisa ser corrigido. É o caso do terminal físico e do cadastro que ainda não tem documento.

Data e hora

A data precisa vir em ISO 8601 com fuso horário e com o T separando data e hora. Fração de segundo é aceita, em qualquer precisão.

2026-08-14T14:30:00Z          OK
2026-08-14T14:30:00.123Z      OK   fração de segundo
2026-08-14T11:30:00.123456Z   OK   qualquer precisão
2026-08-14T11:30:00-03:00     OK   deslocamento em vez de Z
2026-08-14T14:30:00           NAO  sem fuso: o instante fica ambíguo
2026-08-14 14:30:00Z          NAO  sem o T separando data e hora
14/08/2026 14:30              NAO  não é ISO 8601

O fuso é obrigatório porque sem ele o mesmo texto significa horas diferentes conforme quem lê, e a análise compara comportamento entre apostadores no tempo. A data é interpretada de verdade, não conferida por formato, então uma data com a forma certa e impossível no calendário é recusada na resposta da própria requisição.

Data no futuro também é recusada, com um dia de tolerância para relógio adiantado. A data registra quando o fato aconteceu, e um único evento adiantado distorce todo gráfico que calcula média ao longo do tempo. Carga histórica não tem limite de profundidade.

O que mais entra sem erro e não vira score

A leitura dos seus dados é configurada no onboarding, a partir da amostra e da descrição que você envia. O que ficar de fora dessa configuração entra normalmente e não vira indicador, sem erro. São quatro casos.

Tipo de evento fora do combinado. A tradução dos seus tipos para os nove canônicos é configurada por marca. Um tipo que não está nessa configuração é gravado com o nome original e não entra em nenhum indicador. Um tipo novo deve ser informado à Betshield para ser incluído na configuração.

Número em formato que não converte. Valor numérico pode vir como número ou como texto, e "50.00" é interpretado como 50.00. O que não funciona é moeda ou vírgula decimal: "R$ 50,00" entra e o campo fica vazio.

Valor que não foi mencionado no onboarding. Quatro campos alimentam indicadores específicos, e a tradução dos valores deles é configurada por marca junto com o resto. Se a sua operação passar a registrar o resultado de uma aposta ou a forma de pagamento em outro formato, o campo chega sem tradução e o indicador correspondente fica sem dado.

Evento sem identificador do apostador. Ele volta contado em skipped, com o motivo, e a resposta ainda é 200. Um job que só confere o código HTTP não percebe. Acompanhe o número, não só o status.

Rótulo de identificação interno

Envie também um identificador interno do apostador: nome de usuário, código de conta, ID de jogador. Sem ele, os apostadores aparecem no painel apenas como um código de 64 caracteres, e o time de operação não consegue agir sobre ninguém.

Não use o CPF nesse campo. Diferente do campo de identificação, que é convertido em hash, este rótulo é armazenado e exibido exatamente como você enviou, sem transformação. Ele deve ser um identificador do seu sistema, não um dado pessoal.

Cruzar o painel com a sua base

Se você precisa ligar um apostador do painel a um registro seu, calcule o SHA-256 do CPF conforme Como calcular o hash e guarde a correspondência do seu lado. A Betshield não consegue reverter o hash, então essa correspondência só pode existir do seu lado.

Resumo do que preparar

  1. Um campo de identificação do apostador em cada evento: CPF de 11 dígitos, ou o SHA-256 dele.
  2. O tipo do evento, com os nomes que você já usa. A tradução para os nove tipos canônicos é configurada no onboarding.
  3. A data em ISO 8601, com fuso horário e com o T separando data e hora. Fração de segundo é aceita.
  4. Os campos de valor de cada tipo de evento, também com os nomes que você já usa.
  5. Como a sua operação registra o resultado da aposta, a forma de pagamento, o sentido de uma mudança de limite e se a aposta foi ao vivo. Envie os seus valores, e a tradução é configurada aqui.
  6. Um rótulo de identificação interno, que não seja o CPF.
  7. Carga histórica antes do envio contínuo. Sem histórico, o modelo não tem com o que comparar e todo apostador sai com a mesma nota.
  8. No seu job de envio: lotes de até 2.000 eventos, X-Idempotency-Key por lote, e nova tentativa com espera crescente em erro 5xx.

Idempotência

Para evitar processamento duplicado, inclua o header X-Idempotency-Key nas requisições de ingestão. Se a mesma chave for enviada dentro de 24 horas, a resposta original será retornada sem reprocessar os eventos.

X-Idempotency-Key: batch-2026-04-14-001

A chave deve ter no máximo 128 caracteres. Recomendamos usar um identificador único por lote (ex: UUID, timestamp + ID do lote).

Privacidade e Segurança de Dados

A Betshield foi projetada com privacidade como princípio fundamental. Nenhum dado pessoal identificável (PII) é armazenado nos nossos servidores.

MecanismoDescrição
Hash SHA-256CPFs e identificadores de jogadores passam por hash SHA-256 antes de qualquer processamento. O valor original é descartado imediatamente.
Sem PII armazenadaNomes, telefones, emails de apostadores nunca transitam pela API. Trabalhamos exclusivamente com hashes irreversíveis.
Isolamento por operadoraCada operadora possui tabelas dedicadas com isolamento completo. Dados de uma operadora nunca são acessíveis por outra.
Criptografia em trânsitoTodas as conexões são TLS 1.3. Certificados gerenciados automaticamente.
API Keys com hashAs chaves são hasheadas (SHA-256) no momento da criação. O valor em texto plano é exibido apenas uma vez.
LGPD e SIGAP

A arquitetura da Betshield foi desenhada para atender aos requisitos da LGPD e às regulamentações do SIGAP (Sistema de Gestão de Apostas) do Ministério da Fazenda.

Consultar Scores

Scores de risco variam de 0 (baixo risco) a 100 (alto risco) e são recalculados diariamente.

GET /scores

GET /scores

Lista scores de todos os jogadores monitorados, com paginação e filtros.

ParâmetroTipoDefaultDescrição
pageint1Página atual
page_sizeint50Itens por página (máximo 200)
sort_bystringscoreCampo de ordenação: score, scored_at
sort_orderstringdescasc ou desc
risk_levelstring—Filtro: low, medium, high, critical
searchstring—Busca pelo rótulo interno do apostador ou pelo início do código. Até 100 caracteres
brand_idstring—UUID da marca. Obrigatório quando a sua identidade alcança mais de uma marca. Com API key a marca já vem da chave
curl "https://app.thebetshield.com/api/v1/scores?risk_level=high&page_size=20&sort_by=score&sort_order=desc" \
  -H "Authorization: Bearer betshield_key_aBcDeFgH1234567890"
import requests

response = requests.get(
    "https://app.thebetshield.com/api/v1/scores",
    headers={"Authorization": "Bearer betshield_key_aBcDeFgH1234567890"},
    params={
        "risk_level": "high",
        "page_size": 20,
        "sort_by": "score",
        "sort_order": "desc"
    }
)

print(response.json())
const params = new URLSearchParams({
  risk_level: "high",
  page_size: "20",
  sort_by: "score",
  sort_order: "desc"
});

const response = await fetch(
  `https://app.thebetshield.com/api/v1/scores?${params}`,
  {
    headers: {
      "Authorization": "Bearer betshield_key_aBcDeFgH1234567890"
    }
  }
);

const data = await response.json();
using var client = new HttpClient();
client.DefaultRequestHeaders.Add(
    "Authorization", "Bearer betshield_key_aBcDeFgH1234567890"
);

var response = await client.GetAsync(
    "https://app.thebetshield.com/api/v1/scores?risk_level=high&page_size=20&sort_by=score&sort_order=desc"
);

var json = await response.Content.ReadAsStringAsync();
// Resposta 200 OK
{
  "items": [
    {
      "bettor_sha256": "a3f5e9b2c1d4e6f8a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3",
      "display_label": "conta_84213",
      "score": 87,
      "risk_level": "critical",
      "score_reasons": null,
      "features_snapshot": null,
      "scored_at": "2026-04-05T14:32:00Z",
      "brand_id": "019e1d17-2ad6-45a0-9b3c-8c2ab1d4a223",
      "brand_slug": "marca-principal"
    },
    {
      "bettor_sha256": "b4c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6",
      "display_label": "conta_11907",
      "score": 74,
      "risk_level": "high",
      "score_reasons": null,
      "features_snapshot": null,
      "scored_at": "2026-04-05T13:15:00Z",
      "brand_id": "019e1d17-2ad6-45a0-9b3c-8c2ab1d4a223",
      "brand_slug": "marca-principal"
    }
  ],
  "total": 1284,
  "page": 1,
  "page_size": 20
}

GET /scores/{sha256}

GET /scores/{sha256}

Retorna o score detalhado de um apostador, com os fatores que mais pesaram no resultado.

curl https://app.thebetshield.com/api/v1/scores/a3f5e9b2c1d4e6f8a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3 \
  -H "Authorization: Bearer betshield_key_aBcDeFgH1234567890"
import requests

sha256 = "a3f5e9b2c1d4e6f8a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3"
response = requests.get(
    f"https://app.thebetshield.com/api/v1/scores/{sha256}",
    headers={"Authorization": "Bearer betshield_key_aBcDeFgH1234567890"}
)

print(response.json())
const sha256 = "a3f5e9b2c1d4e6f8a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3";

const response = await fetch(
  `https://app.thebetshield.com/api/v1/scores/${sha256}`,
  {
    headers: {
      "Authorization": "Bearer betshield_key_aBcDeFgH1234567890"
    }
  }
);

const data = await response.json();
using var client = new HttpClient();
client.DefaultRequestHeaders.Add(
    "Authorization", "Bearer betshield_key_aBcDeFgH1234567890"
);

var sha256 = "a3f5e9b2c1d4e6f8a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3";
var response = await client.GetAsync(
    $"https://app.thebetshield.com/api/v1/scores/{sha256}"
);

var json = await response.Content.ReadAsStringAsync();
// Resposta 200 OK
{
  "bettor_sha256": "a3f5e9b2c1d4e6f8a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3",
  "display_label": "conta_84213",
  "score": 87,
  "risk_level": "critical",
  "score_reasons": [
    {
      "feature": "loss_chasing_score",
      "label": "Loss chasing",
      "contribution_pct": 34.2,
      "impact": "very_high"
    },
    {
      "feature": "deposit_count_change_pct",
      "label": "Deposit frequency",
      "contribution_pct": 22.8,
      "impact": "very_high"
    },
    {
      "feature": "stake_variability_7d",
      "label": "Stake variability",
      "contribution_pct": 11.4,
      "impact": "high"
    }
  ],
  "features_snapshot": {
    "loss_chasing_score": 0.71,
    "deposit_count_change_pct": 180.0,
    "stake_variability_7d": 2.4
  },
  "scored_at": "2026-04-05T14:32:00Z",
  "model_version": "zscore-v1",
  "brand_id": "019e1d17-2ad6-45a0-9b3c-8c2ab1d4a223",
  "brand_slug": "marca-principal"
}

GET /scores/{sha256}/history

GET /scores/{sha256}/history

Retorna o histórico de scores de um jogador ao longo do tempo.

ParâmetroTipoDefaultDescrição
daysint30Dias de histórico (máximo 365)
curl "https://app.thebetshield.com/api/v1/scores/a3f5e9b2c1d4e6f8a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3/history?days=14" \
  -H "Authorization: Bearer betshield_key_aBcDeFgH1234567890"
import requests

sha256 = "a3f5e9b2c1d4e6f8a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3"
response = requests.get(
    f"https://app.thebetshield.com/api/v1/scores/{sha256}/history",
    headers={"Authorization": "Bearer betshield_key_aBcDeFgH1234567890"},
    params={"days": 14}
)

print(response.json())
const sha256 = "a3f5e9b2c1d4e6f8a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3";

const response = await fetch(
  `https://app.thebetshield.com/api/v1/scores/${sha256}/history?days=14`,
  {
    headers: {
      "Authorization": "Bearer betshield_key_aBcDeFgH1234567890"
    }
  }
);

const data = await response.json();
using var client = new HttpClient();
client.DefaultRequestHeaders.Add(
    "Authorization", "Bearer betshield_key_aBcDeFgH1234567890"
);

var sha256 = "a3f5e9b2c1d4e6f8a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3";
var response = await client.GetAsync(
    $"https://app.thebetshield.com/api/v1/scores/{sha256}/history?days=14"
);

var json = await response.Content.ReadAsStringAsync();
// Resposta 200 OK
{
  "bettor_sha256": "a3f5e9b2c1d4e6f8a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3",
  "brand_id": "019e1d17-2ad6-45a0-9b3c-8c2ab1d4a223",
  "brand_slug": "marca-principal",
  "history": [
    {
      "score": 87,
      "risk_level": "critical",
      "scored_at": "2026-04-05T18:05:00Z"
    },
    {
      "score": 72,
      "risk_level": "high",
      "scored_at": "2026-04-05T12:05:00Z"
    },
    {
      "score": 65,
      "risk_level": "high",
      "scored_at": "2026-04-05T06:05:00Z"
    },
    {
      "score": 58,
      "risk_level": "medium",
      "scored_at": "2026-04-05T00:05:00Z"
    }
  ]
}

Níveis de Risco

Cada jogador recebe um score de 0 a 100, classificado automaticamente em um dos quatro níveis.

ScoreNívelLabel
0 - 30 Baixo low
31 - 60 Médio medium
61 - 80 Alto high
81 - 100 Crítico critical
Por que este score

Cada score vem com a lista dos fatores que mais pesaram, em score_reasons. Cada fator traz o nome interno da característica, um rótulo legível, a participação dele no total em contribution_pct e se ele empurrou o score para cima ou para baixo em impact. Consulte GET /scores/{sha256}.

Webhooks

Em vez de consultar periodicamente quais apostadores tiveram alteração, a sua URL recebe uma requisição POST assinada ao final de cada cálculo de notas. O conteúdo é o mesmo do GET /scores, acrescido da faixa em cinco níveis, entregue por outro transporte, e os dois modelos são complementares: a notificação informa a alteração, e a consulta permanece disponível para conferência e para recuperação de eventuais lacunas.

O cadastro da assinatura é feito no painel, em Webhooks, e não pela API. A restrição é deliberada: essas rotas exigem sessão de usuário e recusam autenticação por API key, de modo que uma chave comprometida não permite redirecionar o seu feed para outro endereço.

Como ativar

  1. No painel, em Webhooks, clique em Novo webhook.
  2. Informe a URL de destino, selecione a marca (ou todas as da licença) e marque os eventos que deseja receber. Para a mudança de faixa, escolha também as faixas de destino e a direção.
  3. Ao salvar, a Betshield envia imediatamente um desafio de posse à URL informada, que precisa devolvê-lo para a assinatura ser ativada.
  4. O cadastro retorna o segredo de assinatura. Ele é exibido uma única vez e não pode ser recuperado depois: armazene-o no seu cofre de segredos antes de fechar a tela.
  5. Caso a URL ainda não estivesse disponível, utilize Verificar de novo. Assinaturas que não confirmam a posse em 24 horas são removidas.

O desafio de posse

O desafio impede que a URL de um terceiro seja cadastrada e passe a receber dados de apostadores. A Betshield efetua um POST na URL informada com o seguinte corpo:

{"type":"webhook.verify","challenge":"chal_9f2c4b7e1a8d3056c4b9e2f7a1d80c35"}

Responda 200 devolvendo o mesmo token, em qualquer um dos dois formatos aceitos:

// como JSON
{"challenge": "chal_9f2c4b7e1a8d3056c4b9e2f7a1d80c35"}

// ou o token puro, como texto
chal_9f2c4b7e1a8d3056c4b9e2f7a1d80c35
O primeiro desafio não é assinado

Não há segredo disponível para assiná-lo: ele é entregue na resposta dessa mesma requisição. Se o seu endpoint rejeita requisições não assinadas, aceite esta e utilize Verificar de novo no painel, pois a segunda tentativa é assinada. O desafio pode ser identificado pelo campo type com valor webhook.verify.

Os eventos

TipoQuando sai
bettor.risk_band.changedO apostador passou para outra faixa de risco, na escala de cinco níveis: very_low, low, medium, high ou critical
bettor.first_scoreO apostador recebeu nota pela primeira vez
run.completedConclusão de um cálculo de notas, emitido uma vez por marca, quatro vezes por dia. É enviado mesmo quando não há alteração
subscription.resync_requiredHá um intervalo que não foi entregue por notificação e precisa ser obtido por consulta. É enviado mesmo sem ter sido selecionado

Todos os eventos utilizam o mesmo envelope, com o conteúdo em data:

{
  "version": "1",
  "type": "bettor.risk_band.changed",
  "timestamp": "2026-09-09T18:05:00Z",
  "data": {
    "bettor_sha256": "a3f1c8e94b2d7605f1a8c3e94b2d7605f1a8c3e94b2d7605f1a8c3e94b2d7605",
    "brand_id": "019e1d17-2ad6-45a0-9b3c-8c2ab1d4a223",
    "brand_slug": "marca-principal",
    "score": 82,
    "score_before": 54,
    "risk_level": "high",
    "risk_level_before": "medium",
    "risk_band": "high",
    "risk_band_before": "medium",
    "score_reasons": [
      {"feature": "game_loss_log", "contribution_pct": 31.4, "impact": "very_high", "label": "Net loss in play"}
    ],
    "scored_at": "2026-09-09T18:05:00Z",
    "change_id": 4193277
  }
}

bettor.first_score possui os mesmos campos, com score_before, risk_level_before e risk_band_before nulos. Os demais possuem data próprio:

// run.completed
{
  "brand_id": "019e1d17-2ad6-45a0-9b3c-8c2ab1d4a223",
  "brand_slug": "marca-principal",
  "run_id": 8814,
  "completed_at": "2026-09-09T18:05:00Z",
  "watermark": 4193277
}

// subscription.resync_required
{
  "reason": "volume",
  "subscription_id": "7c1eb4a0-55d2-4f31-9a6c-2e0b7d81f4aa",
  "brand_id": "019e1d17-2ad6-45a0-9b3c-8c2ab1d4a223",
  "from_cursor": "eyJjIjo0MTgwMDAwLCJ2IjoxfQ",
  "to_cursor": "eyJjIjo0MTkzMjc3LCJ2IjoxfQ"
}
change_id não é a sua posição no feed

Ele serve para ordenar os eventos e descartar duplicados. A ordem de entrega não é garantida: o evento de 900 pode chegar antes do de 880, e armazenar o último recebido como posição de leitura descartaria silenciosamente tudo o que estivesse entre os dois. A posição de consulta é obtida exclusivamente pelo next_cursor do GET /scores/changes.

Faixas e direção

A mudança de faixa e a primeira nota podem ser restritas às faixas que interessam, e a mudança de faixa também ao sentido do movimento. A escolha é por assinatura, no painel, em Webhooks.

EscolhaValoresPadrão
Faixas de destinovery_low, low, medium, high, criticaltodas
Direçãosubidas, descidas ou as duasapenas subidas

A faixa que vale é a de destino: uma assinatura de high e critical recebe quem entrou nessas faixas, e não quem saiu delas. A direção compara a faixa nova com a anterior, então uma assinatura de subidas não recebe descida alguma.

Uma subida é anunciada no cálculo em que acontece. Uma descida é anunciada quando a nota fica três pontos abaixo do piso da faixa que ela deixou, ou quando a faixa nova se mantém em dois cálculos seguidos: sem isso, uma oscilação de um ponto em volta de uma fronteira geraria duas notificações por dia para a mesma pessoa.

Os dois eventos de controle, run.completed e subscription.resync_required, nunca são filtrados. E o filtro vale para a notificação, não para o registro: GET /scores/changes continua devolvendo toda mudança, que é o que permite reconstruir um intervalo perdido.

Verificação da assinatura

A Betshield adota o Standard Webhooks 1.0.0. Toda entrega inclui três headers:

HeaderConteúdo
webhook-idIdentificador do evento, estável entre as tentativas. É o campo utilizado para descartar duplicados
webhook-timestampUnix time em segundos, referente ao momento da tentativa
webhook-signatureUma ou mais assinaturas v1,<base64>, separadas por espaço

O conteúdo assinado é a concatenação dos três valores, separados por ponto, com o corpo exatamente como recebido:

{webhook-id}.{webhook-timestamp}.{corpo cru}

Sobre esse conteúdo aplica-se HMAC-SHA256, utilizando como chave o segredo sem o prefixo whsec_ e decodificado de base64. O resultado é codificado em base64 e prefixado por v1,.

Assine o corpo bruto, nunca o JSON reserializado

A assinatura é calculada sobre os bytes recebidos. Interpretar o corpo como JSON e serializá-lo novamente altera o espaçamento e a ordem das chaves, o que invalida a verificação em todas as entregas. Obtenha o corpo bruto antes do parse.

import base64, hashlib, hmac, time

def verify(secret, webhook_id, timestamp, body, signature_header, tolerance=300):
    # fora da tolerancia, recusa antes de olhar a assinatura
    if abs(int(time.time()) - int(timestamp)) > tolerance:
        return False

    key = base64.b64decode(secret.removeprefix("whsec_"))
    signed = f"{webhook_id}.{timestamp}.{body}".encode()
    expected = "v1," + base64.b64encode(
        hmac.new(key, signed, hashlib.sha256).digest()
    ).decode()

    # durante uma rotacao o header traz duas; basta uma bater
    return any(
        hmac.compare_digest(part, expected)
        for part in signature_header.split(" ") if part
    )
import { createHmac, timingSafeEqual } from "node:crypto";

function verify(secret, webhookId, timestamp, body, signatureHeader, tolerance = 300) {
  // fora da tolerancia, recusa antes de olhar a assinatura
  const now = Math.floor(Date.now() / 1000);
  if (Math.abs(now - Number(timestamp)) > tolerance) return false;

  const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64");
  const signed = `${webhookId}.${timestamp}.${body}`;
  const expected = "v1," + createHmac("sha256", key).update(signed).digest("base64");

  // durante uma rotacao o header traz duas; basta uma bater
  return signatureHeader.split(" ").filter(Boolean).some((received) => {
    const a = Buffer.from(received), b = Buffer.from(expected);
    return a.length === b.length && timingSafeEqual(a, b);
  });
}
Em outras linguagens, utilize a biblioteca oficial

O formato segue o Standard Webhooks, que disponibiliza bibliotecas oficiais para diversas linguagens. Forneça o segredo com o prefixo whsec_, o corpo bruto e os três headers com esses nomes, e a verificação funciona sem adaptações.

Requisitos de resposta

RegraValor
Status de sucessoQualquer 2xx. Status a partir de 300 é tratado como falha, inclusive redirecionamento, que não é seguido
Prazo total10 segundos, contando conexão, TLS e resposta
Tamanho da respostaLida até 64 KiB; o excedente é descartado

Responda antes de processar. Se o processamento puder exceder dez segundos, registre o evento, retorne 200 imediatamente e trate-o de forma assíncrona.

Duplicidade

O mesmo evento pode ser entregue mais de uma vez, comportamento inerente a qualquer mecanismo com retentativa. O webhook-id é estável entre as tentativas de um mesmo evento: armazene os identificadores já processados e descarte as repetições. Uma janela de alguns dias é suficiente.

Retentativas

Uma entrega que falha é repetida 8 vezes, com intervalos de 10, 20, 40, 80, 160, 320 e 480 segundos, totalizando aproximadamente 18 minutos e meio. A janela cobre uma implantação usual do lado do cliente sem perda de eventos.

Esgotadas as tentativas, o evento não é reenviado automaticamente: a Betshield registra o intervalo pendente e o comunica por três canais independentes, o evento subscription.resync_required, o campo resync_required da consulta e a coluna correspondente na tela de Webhooks. Assinaturas que falham por 5 dias consecutivos são desativadas e podem ser restabelecidas pelo botão Reativar.

Recuperação de intervalos não entregues

Há uma situação em que a notificação individual é suprimida deliberadamente: quando um cálculo produz um volume muito acima do normal daquela assinatura, uma requisição por apostador sobrecarregaria o endpoint de destino. Nesse caso é emitido um subscription.resync_required com o intervalo correspondente, e o conteúdo deve ser obtido por consulta.

Os campos from_cursor e to_cursor são cursores opacos, no mesmo formato do next_cursor: informe from_cursor em after e pagine até alcançar to_cursor.

Mudanças registradas antes da escala de cinco níveis trazem change_kind igual a risk_level_changed e risk_band nulo, até que a retenção as alcance.

GET /scores/changes

GET /scores/changes

O mesmo feed em forma de consulta. Permite recuperar intervalos não entregues por notificação e pode ser utilizado isoladamente por quem prefere consultar em vez de receber.

ParâmetroTipoDefaultDescrição
afterstring—Cursor retornado pela chamada anterior. Na ausência dele, a leitura inicia no começo da janela de retenção
limitint100Itens por página, máximo 1.000
brand_idstring—UUID da marca. Obrigatório quando a sua identidade alcança mais de uma. Com API key a marca já vem da chave
curl -H "Authorization: Bearer SUA_API_KEY" \
  "https://app.thebetshield.com/api/v1/scores/changes?limit=100&after=eyJjIjo0MTgwMDAwLCJ2IjoxfQ"
// Resposta 200 OK
{
  "items": [
    {
      "change_id": 4193277,
      "change_kind": "risk_band_changed",
      "bettor_sha256": "a3f1c8e94b2d7605f1a8c3e94b2d7605f1a8c3e94b2d7605f1a8c3e94b2d7605",
      "brand_id": "019e1d17-2ad6-45a0-9b3c-8c2ab1d4a223",
      "brand_slug": "marca-principal",
      "score": 82,
      "score_before": 54,
      "risk_level": "high",
      "risk_level_before": "medium",
      "risk_band": "high",
      "risk_band_before": "medium",
      "score_reasons": [
        {"feature": "game_loss_log", "contribution_pct": 56.1, "impact": "very_high", "label": "Money lost betting"}
      ],
      "run_at": "2026-09-09T18:05:00.412883Z"
    }
  ],
  "next_cursor": "eyJjIjo0MTkzMjc3LCJ2IjoxfQ",
  "has_more": true,
  "watermark": 4193277,
  "last_completed_run_at": "2026-09-09T18:05:00.412883Z",
  "resync_required": false,
  "resync_from": null,
  "resync_to": null
}

Armazene o next_cursor e informe-o em after na chamada seguinte. Enquanto has_more for true, prossiga; quando for false, a leitura alcançou o fim do que está disponível no momento.

Os demais campos permitem verificar que o feed está operante sem depender de nenhum evento. last_completed_run_at indica a conclusão do último cálculo e é retornado inclusive na resposta vazia; watermark indica até que ponto o feed poderia ser servido neste momento, o que distingue a ausência de alterações de uma interrupção do cálculo.

StatusSignificado
400Cursor inválido, ou brand_id obrigatório e ausente
403A sua identidade não alcança a marca solicitada
410O cursor é anterior à janela de retenção. Reinicie a leitura sem after e reconcilie pelo GET /scores
429Acima de 60 requisições por minuto

Rotação do segredo

No painel, em Webhooks, a rotação gera um novo segredo, exibido uma única vez. Durante 24 horas os dois segredos permanecem válidos: cada entrega inclui as duas assinaturas no mesmo header, separadas por espaço, de modo que verificadores com o segredo anterior e com o novo aceitam a mesma requisição. Essa é a janela para substituir o valor no seu cofre de segredos sem perda de entregas. Decorridas as 24 horas, o segredo anterior deixa de assinar.

Requisitos do endpoint

O destino é validado no cadastro e novamente a cada entrega, pois registros DNS são mutáveis: um nome que resolvia para um endereço público no momento do cadastro pode resolver para outro no momento do envio.

ExigênciaPor quê
httpsO corpo transporta código de apostador e nota de risco
Porta 443Somente a porta padrão é aceita
Endereço públicoO nome deve resolver para endereço roteável na internet, e todos os endereços associados a ele são validados
Sem credencial na URLCredenciais no formato usuario:senha@ não são aceitas. A autenticação é feita pela assinatura
Sem redirecionamentoRedirecionamentos não são seguidos e são contabilizados como falha

Cada licença admite até 20 assinaturas. A tela de Webhooks apresenta as entregas recentes de cada uma, com o status retornado e o número de tentativas, mantidas por 90 dias.

Dashboard

O dashboard oferece visão consolidada do risco da sua base de jogadores. Disponível via interface web e via API.

GET /dashboard/kpis

GET /dashboard/kpis

Retorna KPIs agregados: total de apostadores, percentual em risco, score médio e quantidade de alertas críticos.

ParâmetroTipoDefaultDescrição
periodstring—Período dos KPIs: 30d, 90d, 6m, 12m, 2y
curl https://app.thebetshield.com/api/v1/dashboard/kpis \
  -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIs..."
import requests

response = requests.get(
    "https://app.thebetshield.com/api/v1/dashboard/kpis",
    headers={"Authorization": "Bearer eyJhbGciOiJSUzI1NiIs..."}
)

print(response.json())
const response = await fetch(
  "https://app.thebetshield.com/api/v1/dashboard/kpis",
  {
    headers: {
      "Authorization": "Bearer eyJhbGciOiJSUzI1NiIs..."
    }
  }
);

const data = await response.json();
using var client = new HttpClient();
client.DefaultRequestHeaders.Add(
    "Authorization", "Bearer eyJhbGciOiJSUzI1NiIs..."
);

var response = await client.GetAsync(
    "https://app.thebetshield.com/api/v1/dashboard/kpis"
);

var json = await response.Content.ReadAsStringAsync();
// Resposta 200 OK
{
  "total_bettors": 12847,
  "bettors_at_risk": 1043,
  "avg_score": 34.7,
  "critical_count": 127,
  "last_scored_at": "2026-04-05T18:05:00Z",
  "brand_slug": "marca-principal"
}

GET /dashboard/charts/{type}

GET /dashboard/charts/{type}

Retorna dados para gráficos do dashboard, parametrizados pelo tipo de visualização.

ParâmetroTipoLocalDescrição
typestringPathTipo do gráfico (ver Charts Disponíveis)
periodstringQueryPeríodo dos dados: 30d, 90d, 6m, 12m, 2y
granularitystringQueryGranularidade temporal: day, month, year
curl https://app.thebetshield.com/api/v1/dashboard/charts/score_evolution \
  -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIs..."
import requests

chart_type = "score_evolution"
response = requests.get(
    f"https://app.thebetshield.com/api/v1/dashboard/charts/{chart_type}",
    headers={"Authorization": "Bearer eyJhbGciOiJSUzI1NiIs..."}
)

print(response.json())
const chartType = "score_evolution";

const response = await fetch(
  `https://app.thebetshield.com/api/v1/dashboard/charts/${chartType}`,
  {
    headers: {
      "Authorization": "Bearer eyJhbGciOiJSUzI1NiIs..."
    }
  }
);

const data = await response.json();
using var client = new HttpClient();
client.DefaultRequestHeaders.Add(
    "Authorization", "Bearer eyJhbGciOiJSUzI1NiIs..."
);

var chartType = "score_evolution";
var response = await client.GetAsync(
    $"https://app.thebetshield.com/api/v1/dashboard/charts/{chartType}"
);

var json = await response.Content.ReadAsStringAsync();
// Resposta 200 OK — exemplo para score_evolution
{
  "chart_type": "score_evolution",
  "data": [
    {
      "label": "2026-03-30",
      "value": 32.1
    },
    {
      "label": "2026-03-31",
      "value": 33.4
    },
    {
      "label": "2026-04-01",
      "value": 33.9
    },
    {
      "label": "2026-04-02",
      "value": 34.2
    },
    {
      "label": "2026-04-03",
      "value": 34.5
    },
    {
      "label": "2026-04-04",
      "value": 34.6
    },
    {
      "label": "2026-04-05",
      "value": 34.7
    }
  ]
}

Charts Disponíveis

O endpoint GET /dashboard/charts/{type} aceita os seguintes tipos:

TipoDescriçãoDados retornados
risk_distributionDistribuição por nível de risco{label, value}
score_evolutionEvolução do score médio{label, value}
risk_level_trendPercentual por nível de risco ao longo do tempo{label, low, medium, high, critical}
chasing_loss% de apostadores com comportamento de chasing loss{label, value}
bet_gap_distributionIntervalo entre apostas consecutivas, em horas (box plot){label, min, q1, median, q3, max}
monthly_depositsQuantidade de depósitos por período{label, value}
monthly_betsQuantidade de apostas por período{label, value}
monthly_gains_lossesGanhos (retorno menos o valor apostado) e perdas, em reais{label, gains, losses}
score_operator_marketScore da operadora vs média do mercado{label, operator, market}
avg_session_durationTempo de tela médio em horas{label, value}
daily_ingestionVolume de eventos recebidos por dia{label, value}
top_riskApostadores de maior score{label, value}

Equipe

Gerencie os membros da sua organização. Papéis disponíveis: admin e analyst.

GET /users

GET /users

Lista todos os membros da equipe e seus papéis.

curl https://app.thebetshield.com/api/v1/users \
  -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIs..."
import requests

response = requests.get(
    "https://app.thebetshield.com/api/v1/users",
    headers={"Authorization": "Bearer eyJhbGciOiJSUzI1NiIs..."}
)

print(response.json())
const response = await fetch(
  "https://app.thebetshield.com/api/v1/users",
  {
    headers: {
      "Authorization": "Bearer eyJhbGciOiJSUzI1NiIs..."
    }
  }
);

const data = await response.json();
using var client = new HttpClient();
client.DefaultRequestHeaders.Add(
    "Authorization", "Bearer eyJhbGciOiJSUzI1NiIs..."
);

var response = await client.GetAsync(
    "https://app.thebetshield.com/api/v1/users"
);

var json = await response.Content.ReadAsStringAsync();
// Resposta 200 OK
{
  "data": [
    {
      "id": "1a7b3c95-2d48-4e17-8f60-9b5a4c2d7e83",
      "email": "joao@bet.com",
      "name": "João Silva",
      "role": "admin",
      "status": "active",
      "created_at": "2026-01-15T10:30:00Z"
    },
    {
      "id": "8c4d2e15-7a93-4f60-b1e8-5d3c9a7b2f41",
      "email": "maria@bet.com",
      "name": "Maria Santos",
      "role": "analyst",
      "status": "active",
      "created_at": "2026-02-20T14:00:00Z"
    },
    {
      "id": "3f9a1c72-5b84-4e21-9d0f-6a2b7c8e1d34",
      "email": "carlos@bet.com",
      "name": "Carlos Oliveira",
      "role": "analyst",
      "status": "inactive",
      "created_at": "2026-03-01T09:00:00Z"
    }
  ]
}

POST /users

POST /users

Convida um novo membro para a equipe, enviando um link de configuração por email.

CampoTipoObrigatórioDescrição
emailstringSimEmail do novo membro
rolestringSimPapel: admin ou analyst
curl -X POST https://app.thebetshield.com/api/v1/users \
  -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIs..." \
  -H "Content-Type: application/json" \
  -d '{
    "email": "ana@bet.com",
    "role": "analyst"
  }'
import requests

response = requests.post(
    "https://app.thebetshield.com/api/v1/users",
    headers={"Authorization": "Bearer eyJhbGciOiJSUzI1NiIs..."},
    json={
        "email": "ana@bet.com",
        "role": "analyst"
    }
)

print(response.json())
const response = await fetch(
  "https://app.thebetshield.com/api/v1/users",
  {
    method: "POST",
    headers: {
      "Authorization": "Bearer eyJhbGciOiJSUzI1NiIs...",
      "Content-Type": "application/json"
    },
    body: JSON.stringify({
      email: "ana@bet.com",
      role: "analyst"
    })
  }
);

const data = await response.json();
using var client = new HttpClient();
client.DefaultRequestHeaders.Add(
    "Authorization", "Bearer eyJhbGciOiJSUzI1NiIs..."
);

var payload = @"{
  ""email"": ""ana@bet.com"",
  ""role"": ""analyst""
}";

var content = new StringContent(
    payload, Encoding.UTF8, "application/json"
);

var response = await client.PostAsync(
    "https://app.thebetshield.com/api/v1/users", content
);

var json = await response.Content.ReadAsStringAsync();
// Resposta 200 OK
{
  "id": "6e2f8a41-9c35-4b70-a2d9-3f8e1b6c5a97",
  "email": "ana@bet.com",
  "role": "analyst",
  "status": "pending",
  "setup_link": "https://app.thebetshield.com/setup/tok_AbCdEfGh1234567890",
  "expires_at": "2026-04-12T23:59:59Z"
}

PUT /users/{id}/deactivate

PUT /users/{id}/deactivate

Desativa um membro da equipe, revogando o acesso imediatamente.

curl -X PUT https://app.thebetshield.com/api/v1/users/3f9a1c72-5b84-4e21-9d0f-6a2b7c8e1d34/deactivate \
  -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIs..."
import requests

user_id = "3f9a1c72-5b84-4e21-9d0f-6a2b7c8e1d34"
response = requests.put(
    f"https://app.thebetshield.com/api/v1/users/{user_id}/deactivate",
    headers={"Authorization": "Bearer eyJhbGciOiJSUzI1NiIs..."}
)

print(response.json())
const userId = "3f9a1c72-5b84-4e21-9d0f-6a2b7c8e1d34";

const response = await fetch(
  `https://app.thebetshield.com/api/v1/users/${userId}/deactivate`,
  {
    method: "PUT",
    headers: {
      "Authorization": "Bearer eyJhbGciOiJSUzI1NiIs..."
    }
  }
);

const data = await response.json();
using var client = new HttpClient();
client.DefaultRequestHeaders.Add(
    "Authorization", "Bearer eyJhbGciOiJSUzI1NiIs..."
);

var userId = "3f9a1c72-5b84-4e21-9d0f-6a2b7c8e1d34";
var response = await client.PutAsync(
    $"https://app.thebetshield.com/api/v1/users/{userId}/deactivate", null
);

var json = await response.Content.ReadAsStringAsync();
// Resposta 200 OK
{
  "id": "3f9a1c72-5b84-4e21-9d0f-6a2b7c8e1d34",
  "email": "carlos@bet.com",
  "status": "inactive",
  "deactivated_at": "2026-04-05T16:00:00Z"
}

PUT /users/{id}/activate

PUT /users/{id}/activate

Reativa um membro previamente desativado, restaurando o acesso.

curl -X PUT https://app.thebetshield.com/api/v1/users/3f9a1c72-5b84-4e21-9d0f-6a2b7c8e1d34/activate \
  -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIs..."
import requests

user_id = "3f9a1c72-5b84-4e21-9d0f-6a2b7c8e1d34"
response = requests.put(
    f"https://app.thebetshield.com/api/v1/users/{user_id}/activate",
    headers={"Authorization": "Bearer eyJhbGciOiJSUzI1NiIs..."}
)

print(response.json())
const userId = "3f9a1c72-5b84-4e21-9d0f-6a2b7c8e1d34";

const response = await fetch(
  `https://app.thebetshield.com/api/v1/users/${userId}/activate`,
  {
    method: "PUT",
    headers: {
      "Authorization": "Bearer eyJhbGciOiJSUzI1NiIs..."
    }
  }
);

const data = await response.json();
using var client = new HttpClient();
client.DefaultRequestHeaders.Add(
    "Authorization", "Bearer eyJhbGciOiJSUzI1NiIs..."
);

var userId = "3f9a1c72-5b84-4e21-9d0f-6a2b7c8e1d34";
var response = await client.PutAsync(
    $"https://app.thebetshield.com/api/v1/users/{userId}/activate", null
);

var json = await response.Content.ReadAsStringAsync();
// Resposta 200 OK
{
  "id": "3f9a1c72-5b84-4e21-9d0f-6a2b7c8e1d34",
  "email": "carlos@bet.com",
  "status": "active",
  "activated_at": "2026-04-05T17:00:00Z"
}

PUT /users/{id}/reset

PUT /users/{id}/reset

Reseta a senha de um membro e gera um novo link de configuração.

curl -X PUT https://app.thebetshield.com/api/v1/users/8c4d2e15-7a93-4f60-b1e8-5d3c9a7b2f41/reset \
  -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIs..."
import requests

user_id = "8c4d2e15-7a93-4f60-b1e8-5d3c9a7b2f41"
response = requests.put(
    f"https://app.thebetshield.com/api/v1/users/{user_id}/reset",
    headers={"Authorization": "Bearer eyJhbGciOiJSUzI1NiIs..."}
)

print(response.json())
const userId = "8c4d2e15-7a93-4f60-b1e8-5d3c9a7b2f41";

const response = await fetch(
  `https://app.thebetshield.com/api/v1/users/${userId}/reset`,
  {
    method: "PUT",
    headers: {
      "Authorization": "Bearer eyJhbGciOiJSUzI1NiIs..."
    }
  }
);

const data = await response.json();
using var client = new HttpClient();
client.DefaultRequestHeaders.Add(
    "Authorization", "Bearer eyJhbGciOiJSUzI1NiIs..."
);

var userId = "8c4d2e15-7a93-4f60-b1e8-5d3c9a7b2f41";
var response = await client.PutAsync(
    $"https://app.thebetshield.com/api/v1/users/{userId}/reset", null
);

var json = await response.Content.ReadAsStringAsync();
// Resposta 200 OK
{
  "id": "8c4d2e15-7a93-4f60-b1e8-5d3c9a7b2f41",
  "email": "maria@bet.com",
  "setup_link": "https://app.thebetshield.com/setup/tok_ReSetAbCd9876543210",
  "expires_at": "2026-04-12T23:59:59Z"
}

DELETE /users/{id}

DELETE /users/{id}

Remove permanentemente um membro desativado. O membro deve estar desativado antes da remoção.

Atenção

Esta ação é irreversível. Apenas membros desativados podem ser removidos.

curl -X DELETE https://app.thebetshield.com/api/v1/users/3f9a1c72-5b84-4e21-9d0f-6a2b7c8e1d34 \
  -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIs..."
import requests

user_id = "3f9a1c72-5b84-4e21-9d0f-6a2b7c8e1d34"
response = requests.delete(
    f"https://app.thebetshield.com/api/v1/users/{user_id}",
    headers={"Authorization": "Bearer eyJhbGciOiJSUzI1NiIs..."}
)

print(response.status_code)  # 200
const userId = "3f9a1c72-5b84-4e21-9d0f-6a2b7c8e1d34";

const response = await fetch(
  `https://app.thebetshield.com/api/v1/users/${userId}`,
  {
    method: "DELETE",
    headers: {
      "Authorization": "Bearer eyJhbGciOiJSUzI1NiIs..."
    }
  }
);
// response.status === 200
using var client = new HttpClient();
client.DefaultRequestHeaders.Add(
    "Authorization", "Bearer eyJhbGciOiJSUzI1NiIs..."
);

var userId = "3f9a1c72-5b84-4e21-9d0f-6a2b7c8e1d34";
var response = await client.DeleteAsync(
    $"https://app.thebetshield.com/api/v1/users/{userId}"
);
// response.StatusCode == HttpStatusCode.OK
// Resposta 200 OK
{
  "message": "User deleted"
}

Marcas

A Betshield organiza dados em duas camadas: Operadora (tenant) e Marca (brand). A operadora é a sua empresa (CNPJ contratante). Uma operadora pode ter uma ou mais marcas comerciais, cada uma com o próprio site e a própria base de apostadores.

ConceitoO que é
OperadoraA empresa contratante (CNPJ). Tem um único contrato com a Betshield.
MarcaUma identidade comercial dentro da operadora. Cada marca tem suas próprias API keys e seu próprio score por apostador.
Marca padrãoToda operadora começa com uma marca padrão (mesmo nome da operadora). Operadoras com uma única marca não precisam configurar nada adicional.
Operadora com uma única marca

Em uma operadora com marca única o conceito é invisível: há uma API key e um dashboard, como antes. As seções abaixo são relevantes apenas para operadoras com múltiplas marcas.

A marca não é enviada no payload

A marca de cada evento é derivada automaticamente da API key utilizada. Cada API key pertence a exatamente uma marca. Uma operadora com duas marcas precisa de duas API keys, uma por marca, e envia os eventos de cada marca pela key correspondente.

Importante

Campos brand_slug ou brand_id presentes no payload de POST /ingest são silenciosamente ignorados. A marca vem da API key, não do payload.

Mesma pessoa, marcas diferentes

Quando um mesmo apostador (mesmo CPF) joga em duas marcas da operadora, a Betshield calcula scores separados para cada marca. Isso reflete que o comportamento de risco pode variar entre marcas (limites diferentes, perfil de produto distinto). No dashboard, a consulta de scores é sempre filtrada por marca.

Acesso a marcas

Usuários (admins e analistas) podem ter acesso a todas as marcas da operadora ou a um subconjunto explícito. O administrador da operadora define o escopo de cada usuário no dashboard. Veja a tabela em Permissões para detalhes.

API Keys

Gerencie as chaves de API da sua organização. A chave completa só é exibida uma vez, no momento da criação.

API Keys são por marca

Toda API key pertence a exatamente uma marca. Ao criar uma key, escolha a marca a que ela está vinculada. Eventos enviados com essa key são automaticamente atribuídos a essa marca.

GET /api-keys

GET /api-keys

Lista todas as API Keys da organização, mostrando prefixo, status e data de expiração.

curl https://app.thebetshield.com/api/v1/api-keys \
  -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIs..."
import requests

response = requests.get(
    "https://app.thebetshield.com/api/v1/api-keys",
    headers={"Authorization": "Bearer eyJhbGciOiJSUzI1NiIs..."}
)

print(response.json())
const response = await fetch(
  "https://app.thebetshield.com/api/v1/api-keys",
  {
    headers: {
      "Authorization": "Bearer eyJhbGciOiJSUzI1NiIs..."
    }
  }
);

const data = await response.json();
using var client = new HttpClient();
client.DefaultRequestHeaders.Add(
    "Authorization", "Bearer eyJhbGciOiJSUzI1NiIs..."
);

var response = await client.GetAsync(
    "https://app.thebetshield.com/api/v1/api-keys"
);

var json = await response.Content.ReadAsStringAsync();
// Resposta 200 OK
{
  "data": [
    {
      "id": "5a8e3f07-1d62-4c94-b7a0-8f2e6b3d9c15",
      "name": "Produção - Ingestão",
      "prefix": "betshield_key_aBcD...",
      "status": "active",
      "created_at": "2026-01-20T10:00:00Z",
      "expires_at": "2027-01-20T10:00:00Z",
      "last_used_at": "2026-04-05T14:30:00Z"
    },
    {
      "id": "b4e7d219-6f03-4a58-9c21-7e5b8d4a3f16",
      "name": "Staging - Testes",
      "prefix": "betshield_key_xYzW...",
      "status": "active",
      "created_at": "2026-03-10T15:00:00Z",
      "expires_at": "2026-06-10T15:00:00Z",
      "last_used_at": "2026-04-04T09:15:00Z"
    }
  ]
}

POST /api-keys

POST /api-keys

Cria uma nova API Key; o valor completo (full_key) só é exibido uma vez nesta resposta.

CampoTipoObrigatórioDescrição
namestringSimNome descritivo da chave
brand_idstringSimUUID da marca. Cada chave vale para uma marca só, e os eventos enviados com ela entram nessa marca
expires_in_daysintNãoDias até expirar, de 1 a 365. Sem este campo, a chave não expira.
curl -X POST https://app.thebetshield.com/api/v1/api-keys \
  -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIs..." \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Produção - Pipeline v2",
    "brand_id": "019e1d17-2ad6-45a0-9b3c-8c2ab1d4a223",
    "expires_in_days": 90
  }'
import requests

response = requests.post(
    "https://app.thebetshield.com/api/v1/api-keys",
    headers={"Authorization": "Bearer eyJhbGciOiJSUzI1NiIs..."},
    json={
        "name": "Produção - Pipeline v2",
        "brand_id": "019e1d17-2ad6-45a0-9b3c-8c2ab1d4a223",
        "expires_in_days": 90
    }
)

data = response.json()
print("Salve esta chave!", data["full_key"])
const response = await fetch(
  "https://app.thebetshield.com/api/v1/api-keys",
  {
    method: "POST",
    headers: {
      "Authorization": "Bearer eyJhbGciOiJSUzI1NiIs...",
      "Content-Type": "application/json"
    },
    body: JSON.stringify({
      name: "Produção - Pipeline v2",
      brand_id: "019e1d17-2ad6-45a0-9b3c-8c2ab1d4a223",
      expires_in_days: 90
    })
  }
);

const data = await response.json();
console.log("Salve esta chave!", data.full_key);
using var client = new HttpClient();
client.DefaultRequestHeaders.Add(
    "Authorization", "Bearer eyJhbGciOiJSUzI1NiIs..."
);

var payload = @"{
  ""name"": ""Produção - Pipeline v2"",
  ""brand_id"": ""019e1d17-2ad6-45a0-9b3c-8c2ab1d4a223"",
  ""expires_in_days"": 90
}";

var content = new StringContent(
    payload, Encoding.UTF8, "application/json"
);

var response = await client.PostAsync(
    "https://app.thebetshield.com/api/v1/api-keys", content
);

var json = await response.Content.ReadAsStringAsync();
Atenção

O campo full_key só é retornado nesta resposta. Copie e armazene a chave em local seguro antes de fechar esta página.

// Resposta 200 OK
{
  "id": "d9c1a836-4b72-4e05-8f39-2a6d7c1b9e54",
  "name": "Produção - Pipeline v2",
  "prefix": "betshield_key_nEwK...",
  "full_key": "betshield_key_nEwKeY1234567890AbCdEfGhIjKlMnOpQrStUvWx",
  "status": "active",
  "created_at": "2026-04-05T18:00:00Z",
  "expires_at": "2026-07-04T18:00:00Z"
}

DELETE /api-keys/{id}

DELETE /api-keys/{id}

Revoga uma API Key imediatamente; todas as requisições com esta chave passam a retornar 401.

curl -X DELETE https://app.thebetshield.com/api/v1/api-keys/b4e7d219-6f03-4a58-9c21-7e5b8d4a3f16 \
  -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIs..."
import requests

key_id = "b4e7d219-6f03-4a58-9c21-7e5b8d4a3f16"
response = requests.delete(
    f"https://app.thebetshield.com/api/v1/api-keys/{key_id}",
    headers={"Authorization": "Bearer eyJhbGciOiJSUzI1NiIs..."}
)

print(response.status_code)  # 200
const keyId = "b4e7d219-6f03-4a58-9c21-7e5b8d4a3f16";

const response = await fetch(
  `https://app.thebetshield.com/api/v1/api-keys/${keyId}`,
  {
    method: "DELETE",
    headers: {
      "Authorization": "Bearer eyJhbGciOiJSUzI1NiIs..."
    }
  }
);
// response.status === 200
using var client = new HttpClient();
client.DefaultRequestHeaders.Add(
    "Authorization", "Bearer eyJhbGciOiJSUzI1NiIs..."
);

var keyId = "b4e7d219-6f03-4a58-9c21-7e5b8d4a3f16";
var response = await client.DeleteAsync(
    $"https://app.thebetshield.com/api/v1/api-keys/{keyId}"
);
// response.StatusCode == HttpStatusCode.OK
// Resposta 200 OK
{
  "message": "API key revoked"
}

DELETE /api-keys/{id}/permanent

DELETE /api-keys/{id}/permanent

Remove permanentemente uma API key revogada. A key deve estar revogada antes da remoção.

curl -X DELETE https://app.thebetshield.com/api/v1/api-keys/b4e7d219-6f03-4a58-9c21-7e5b8d4a3f16/permanent \
  -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIs..."
import requests

key_id = "b4e7d219-6f03-4a58-9c21-7e5b8d4a3f16"
response = requests.delete(
    f"https://app.thebetshield.com/api/v1/api-keys/{key_id}/permanent",
    headers={"Authorization": "Bearer eyJhbGciOiJSUzI1NiIs..."}
)

print(response.status_code)  # 200
const keyId = "b4e7d219-6f03-4a58-9c21-7e5b8d4a3f16";

const response = await fetch(
  `https://app.thebetshield.com/api/v1/api-keys/${keyId}/permanent`,
  {
    method: "DELETE",
    headers: {
      "Authorization": "Bearer eyJhbGciOiJSUzI1NiIs..."
    }
  }
);
// response.status === 200
using var client = new HttpClient();
client.DefaultRequestHeaders.Add(
    "Authorization", "Bearer eyJhbGciOiJSUzI1NiIs..."
);

var keyId = "b4e7d219-6f03-4a58-9c21-7e5b8d4a3f16";
var response = await client.DeleteAsync(
    $"https://app.thebetshield.com/api/v1/api-keys/{keyId}/permanent"
);
// response.StatusCode == HttpStatusCode.OK
// Resposta 200 OK
{
  "message": "API key permanently deleted"
}

Endpoints

MétodoPathDescrição
POST/auth/loginAutenticação JWT
POST/auth/refreshRenovar token
POST/auth/logoutRevogar sessão
GET/auth/mePerfil do usuário
GET/auth/setup/{token}Validar link de setup
POST/auth/setup/{token}Criar conta via convite
POST/ingestEnviar eventos
GET/scoresListar scores
GET/scores/{sha256}Score individual com os fatores que pesaram
GET/scores/{sha256}/historyHistórico de scores
GET/scores/changesFeed de mudanças de score, por cursor
GET/dashboard/kpisKPIs agregados
GET/dashboard/charts/{type}Dados para gráficos
GET/usersListar membros
POST/tenants/{tid}/usersConvidar membro da equipe
POST/usersCriar outro superadministrador da Betshield. Não é o convite de membro
PUT/users/{id}/deactivateDesativar membro
PUT/users/{id}/activateReativar membro
PUT/users/{id}/resetResetar senha
DELETE/users/{id}Remover membro desativado
GET/api-keysListar API Keys
POST/api-keysCriar API Key
DELETE/api-keys/{id}Revogar API Key
DELETE/api-keys/{id}/permanentRemover API Key revogada
GET/healthzStatus de saúde da API

GET /healthz

GET /healthz

Retorna o status de saúde da API. Não requer autenticação.

curl https://app.thebetshield.com/api/v1/healthz
// Resposta 200 OK
{
  "status": "healthy",
  "version": "0.1.0",
  "checks": {
    "postgres": "ok",
    "redis": "ok",
    "every_tenant_has_brand": "ok"
  }
}

Permissões

O acesso aos endpoints varia de acordo com o tipo de autenticação utilizada e o escopo de marca configurado.

AçãoAdminAnalistaAPI Key
Visualizar dashboard e KPIsSimSimNão
Consultar scoresSimSimSim
Enviar eventos (ingestão)SimNãoSim
Gerenciar equipeSimNãoNão
Gerenciar API keysSimNãoNão
Remover membros desativadosSimNãoNão
Remover API keys revogadasSimNãoNão

Escopo de marca

IdentidadeEscopoComportamento
Admin (todas as marcas)Todas as marcas da operadoraVê e age em qualquer marca. Pode delegar acesso parcial a outros usuários.
Admin (parcial)Lista explícita de marcasSó age nas marcas atribuídas. Não pode dar acesso a marcas fora do próprio escopo.
Analista (todas as marcas)Todas as marcas da operadoraLê dados de qualquer marca. Não pode mudar configurações.
Analista (parcial)Lista explícita de marcasLê dados apenas das marcas atribuídas.
API KeySempre uma única marcaEventos enviados com a key são atribuídos à marca dela. Consultas de score retornam apenas dados dessa marca.
Analista

O perfil Analista tem acesso somente leitura ao dashboard e scores. Não pode enviar eventos, gerenciar equipe ou API keys.

Marcas inativas

Quando uma marca é desativada, suas API keys são desativadas em cascata e usuários cujo único acesso era a essa marca perdem acesso ao dashboard. Os dados históricos da marca são preservados e voltam a aparecer quando ela é reativada.

Isolamento de Dados

Cada operadora possui um ambiente completamente isolado. Dados de uma operadora nunca são acessíveis por outra. Dentro da operadora, marcas têm scoring separado mas compartilham infraestrutura física.

MecanismoDescrição
Isolamento físico (operadora)Eventos, scores e features são armazenados em tabelas dedicadas por operadora. Não há compartilhamento de dados entre operadoras.
Isolamento lógico (marca)Dentro de uma operadora, cada linha carrega o brand_id da marca que a originou. Uma API key enxerga só a marca dela. Um usuário do painel enxerga as marcas a que tem acesso, e quando tem acesso a mais de uma pode ver o agregado delas, nunca o de outra operadora.
API Keys por marcaCada API key está vinculada a uma única operadora E uma única marca. Uma key não pode acessar dados de outra operadora nem de outra marca.
Usuários por operadoraAdmins e analistas pertencem a uma operadora. Visualizam apenas dados da operadora e, dentro dela, apenas das marcas a que têm acesso.
Benchmark anônimoO único dado cross-operadora é o score médio do mercado, calculado de forma agregada e anônima. Nenhuma operadora ou marca individual é identificável.

Códigos de Erro

CódigoSignificadoDescrição
400Bad RequestCorpo da requisição inválido ou campos obrigatórios ausentes
401UnauthorizedAPI Key ou token JWT ausente ou inválido
403ForbiddenSem permissão para o recurso solicitado
404Not FoundRecurso não encontrado
409ConflictRecurso duplicado (email já cadastrado, nome de chave já existe)
410GoneLink de ativação expirado
422UnprocessableDados sintaticamente corretos mas semanticamente inválidos
429Too Many RequestsRate limit excedido
500Internal ErrorErro interno do servidor

Todas as respostas de erro seguem o formato:

{
  "error": "INVALID_EVENT_TYPE",
  "message": "O campo event_type deve ser um dos tipos válidos.",
  "request_id": "unknown"
}

Rate Limits

Cada operadora possui um limite de requisições por minuto configurado durante o onboarding. O limite padrão é de 3.000 req/min para ingestão de eventos, e conta requisições, não eventos: como cada requisição leva até 2.000 eventos, agrupar faz o limite deixar de importar.

EndpointLimite padrãoJanela
POST /ingest3.000 req/minToken bucket, por chave
GET /scores/changes60 req/minToken bucket, por chave
POST /auth/login5 tentativas / 15 minPor email
GET/POST /auth/setup10 tentativas/minPor IP, em cada instância

O limite do endpoint de ativação é aplicado por instância do serviço, e serve como proteção adicional. A segurança do link de ativação vem do próprio token, de 256 bits, e do fato de ele ser de uso único.

Quando o limite é excedido, a API retorna 429 com o header Retry-After, em segundos. O valor é a espera real por uma vaga, normalmente 1 segundo, e não a janela inteira. Uma requisição recusada não consome nada do seu limite, então repetir depois do Retry-After é seguro.

Headers de Rate Limit

Respostas do endpoint de ingestão incluem X-RateLimit-Limit, X-RateLimit-Remaining e X-RateLimit-Reset para monitoramento proativo.