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:
- Obtenha sua API Key no dashboard em Configurações > API Keys
- Envie eventos de aposta, depósito e saque via
POST /ingest - 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
Autentica um usuário com email e senha, retornando tokens de acesso e refresh.
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
email | string | Sim | Email do usuário |
password | string | Sim | Senha 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
Renova o token de acesso usando o refresh token.
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
refresh_token | string | Sim | Refresh 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
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
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}
Valida um link de configuração de conta e retorna o email associado.
| Parâmetro | Tipo | Local | Descrição |
|---|---|---|---|
token | string | Path | Token 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}
Cria a conta do usuário convidado, definindo nome e senha, e retorna tokens de acesso.
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
token | string | Sim | Token de setup (path) |
name | string | Sim | Nome completo do usuário |
password | string | Sim | Senha (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
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" }
| Campo | Tipo | Descrição |
|---|---|---|
bettor_sha256 | string | Quem é 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_type | string | O 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. |
timestamp | string | Quando 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.
- Remova tudo que não for dígito. Ponto, hífen e espaço saem.
- Confira que sobraram exatamente 11 dígitos, com os zeros à esquerda preservados.
- Aplique SHA-256 sobre esses 11 dígitos, codificados em UTF-8.
- 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 entrada | SHA-256 esperado |
|---|---|
123.456.789-09 | 7ec94663084bd506d4f0c3e21042df233681fd7426e93f397c921b1d3e397bba |
12345678909 | 7ec94663084bd506d4f0c3e21042df233681fd7426e93f397c921b1d3e397bba |
012.345.678-90 | ee29eb4a8725678278ac439cf7abfd2a849cdc7378a6b6316017b81c51d720e7 |
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.
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.
| Campo | Tipo | O que é, se você tiver |
|---|---|---|
stake | number | Valor apostado, em reais |
odds | number | Cotação da aposta |
sport | string | Esporte da aposta |
in_play | boolean | Se a aposta foi ao vivo |
amount | number | Valor do depósito ou do saque, em reais |
method | string | Meio de pagamento |
result | string | Resultado da aposta liquidada |
payout | number | Valor de retorno, em reais |
bet_ref_id | string | Identificador da aposta na sua plataforma |
limit_type | string | Que limite foi alterado |
limit_value | number | O novo valor do limite |
limit_direction | string | Se o limite subiu ou desceu |
O envelope
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
events | array | Sim | Array 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
- Um campo de identificação do apostador em cada evento: CPF de 11 dígitos, ou o SHA-256 dele.
- O tipo do evento, com os nomes que você já usa. A tradução para os nove tipos canônicos é configurada no onboarding.
- A data em ISO 8601, com fuso horário e com o T separando data e hora. Fração de segundo é aceita.
- Os campos de valor de cada tipo de evento, também com os nomes que você já usa.
- 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.
- Um rótulo de identificação interno, que não seja o CPF.
- 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.
- No seu job de envio: lotes de até 2.000 eventos,
X-Idempotency-Keypor 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.
| Mecanismo | Descrição |
|---|---|
| Hash SHA-256 | CPFs e identificadores de jogadores passam por hash SHA-256 antes de qualquer processamento. O valor original é descartado imediatamente. |
| Sem PII armazenada | Nomes, telefones, emails de apostadores nunca transitam pela API. Trabalhamos exclusivamente com hashes irreversíveis. |
| Isolamento por operadora | Cada operadora possui tabelas dedicadas com isolamento completo. Dados de uma operadora nunca são acessíveis por outra. |
| Criptografia em trânsito | Todas as conexões são TLS 1.3. Certificados gerenciados automaticamente. |
| API Keys com hash | As chaves são hasheadas (SHA-256) no momento da criação. O valor em texto plano é exibido apenas uma vez. |
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
Lista scores de todos os jogadores monitorados, com paginação e filtros.
| Parâmetro | Tipo | Default | Descrição |
|---|---|---|---|
page | int | 1 | Página atual |
page_size | int | 50 | Itens por página (máximo 200) |
sort_by | string | score | Campo de ordenação: score, scored_at |
sort_order | string | desc | asc ou desc |
risk_level | string | — | Filtro: low, medium, high, critical |
search | string | — | Busca pelo rótulo interno do apostador ou pelo início do código. Até 100 caracteres |
brand_id | string | — | 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}
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
Retorna o histórico de scores de um jogador ao longo do tempo.
| Parâmetro | Tipo | Default | Descrição |
|---|---|---|---|
days | int | 30 | Dias 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.
| Score | Nível | Label |
|---|---|---|
0 - 30 |
Baixo | low |
31 - 60 |
Médio | medium |
61 - 80 |
Alto | high |
81 - 100 |
Crítico | critical |
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
- No painel, em Webhooks, clique em Novo webhook.
- 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.
- Ao salvar, a Betshield envia imediatamente um desafio de posse à URL informada, que precisa devolvê-lo para a assinatura ser ativada.
- 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.
- 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
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
| Tipo | Quando sai |
|---|---|
bettor.risk_band.changed | O apostador passou para outra faixa de risco, na escala de cinco níveis: very_low, low, medium, high ou critical |
bettor.first_score | O apostador recebeu nota pela primeira vez |
run.completed | Conclusã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_required | Há 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 feedEle 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.
| Escolha | Valores | Padrão |
|---|---|---|
| Faixas de destino | very_low, low, medium, high, critical | todas |
| Direção | subidas, descidas ou as duas | apenas 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:
| Header | Conteúdo |
|---|---|
webhook-id | Identificador do evento, estável entre as tentativas. É o campo utilizado para descartar duplicados |
webhook-timestamp | Unix time em segundos, referente ao momento da tentativa |
webhook-signature | Uma 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,.
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); }); }
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
| Regra | Valor |
|---|---|
| Status de sucesso | Qualquer 2xx. Status a partir de 300 é tratado como falha, inclusive redirecionamento, que não é seguido |
| Prazo total | 10 segundos, contando conexão, TLS e resposta |
| Tamanho da resposta | Lida 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
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âmetro | Tipo | Default | Descrição |
|---|---|---|---|
after | string | — | Cursor retornado pela chamada anterior. Na ausência dele, a leitura inicia no começo da janela de retenção |
limit | int | 100 | Itens por página, máximo 1.000 |
brand_id | string | — | 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.
| Status | Significado |
|---|---|
400 | Cursor inválido, ou brand_id obrigatório e ausente |
403 | A sua identidade não alcança a marca solicitada |
410 | O cursor é anterior à janela de retenção. Reinicie a leitura sem after e reconcilie pelo GET /scores |
429 | Acima 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ência | Por quê |
|---|---|
https | O corpo transporta código de apostador e nota de risco |
| Porta 443 | Somente a porta padrão é aceita |
| Endereço público | O nome deve resolver para endereço roteável na internet, e todos os endereços associados a ele são validados |
| Sem credencial na URL | Credenciais no formato usuario:senha@ não são aceitas. A autenticação é feita pela assinatura |
| Sem redirecionamento | Redirecionamentos 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
Retorna KPIs agregados: total de apostadores, percentual em risco, score médio e quantidade de alertas críticos.
| Parâmetro | Tipo | Default | Descrição |
|---|---|---|---|
period | string | — | 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}
Retorna dados para gráficos do dashboard, parametrizados pelo tipo de visualização.
| Parâmetro | Tipo | Local | Descrição |
|---|---|---|---|
type | string | Path | Tipo do gráfico (ver Charts Disponíveis) |
period | string | Query | Período dos dados: 30d, 90d, 6m, 12m, 2y |
granularity | string | Query | Granularidade 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:
| Tipo | Descrição | Dados retornados |
|---|---|---|
risk_distribution | Distribuição por nível de risco | {label, value} |
score_evolution | Evolução do score médio | {label, value} |
risk_level_trend | Percentual 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_distribution | Intervalo entre apostas consecutivas, em horas (box plot) | {label, min, q1, median, q3, max} |
monthly_deposits | Quantidade de depósitos por período | {label, value} |
monthly_bets | Quantidade de apostas por período | {label, value} |
monthly_gains_losses | Ganhos (retorno menos o valor apostado) e perdas, em reais | {label, gains, losses} |
score_operator_market | Score da operadora vs média do mercado | {label, operator, market} |
avg_session_duration | Tempo de tela médio em horas | {label, value} |
daily_ingestion | Volume de eventos recebidos por dia | {label, value} |
top_risk | Apostadores de maior score | {label, value} |
Equipe
Gerencie os membros da sua organização. Papéis disponíveis: admin e analyst.
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
Convida um novo membro para a equipe, enviando um link de configuração por email.
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
email | string | Sim | Email do novo membro |
role | string | Sim | Papel: 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
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
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
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}
Remove permanentemente um membro desativado. O membro deve estar desativado antes da remoçã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.
| Conceito | O que é |
|---|---|
| Operadora | A empresa contratante (CNPJ). Tem um único contrato com a Betshield. |
| Marca | Uma identidade comercial dentro da operadora. Cada marca tem suas próprias API keys e seu próprio score por apostador. |
| Marca padrão | Toda operadora começa com uma marca padrão (mesmo nome da operadora). Operadoras com uma única marca não precisam configurar nada adicional. |
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.
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.
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
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
Cria uma nova API Key; o valor completo (full_key) só é exibido uma vez nesta resposta.
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
name | string | Sim | Nome descritivo da chave |
brand_id | string | Sim | UUID da marca. Cada chave vale para uma marca só, e os eventos enviados com ela entram nessa marca |
expires_in_days | int | Não | Dias 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();
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}
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
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étodo | Path | Descrição |
|---|---|---|
| POST | /auth/login | Autenticação JWT |
| POST | /auth/refresh | Renovar token |
| POST | /auth/logout | Revogar sessão |
| GET | /auth/me | Perfil do usuário |
| GET | /auth/setup/{token} | Validar link de setup |
| POST | /auth/setup/{token} | Criar conta via convite |
| POST | /ingest | Enviar eventos |
| GET | /scores | Listar scores |
| GET | /scores/{sha256} | Score individual com os fatores que pesaram |
| GET | /scores/{sha256}/history | Histórico de scores |
| GET | /scores/changes | Feed de mudanças de score, por cursor |
| GET | /dashboard/kpis | KPIs agregados |
| GET | /dashboard/charts/{type} | Dados para gráficos |
| GET | /users | Listar membros |
| POST | /tenants/{tid}/users | Convidar membro da equipe |
| POST | /users | Criar outro superadministrador da Betshield. Não é o convite de membro |
| PUT | /users/{id}/deactivate | Desativar membro |
| PUT | /users/{id}/activate | Reativar membro |
| PUT | /users/{id}/reset | Resetar senha |
| DELETE | /users/{id} | Remover membro desativado |
| GET | /api-keys | Listar API Keys |
| POST | /api-keys | Criar API Key |
| DELETE | /api-keys/{id} | Revogar API Key |
| DELETE | /api-keys/{id}/permanent | Remover API Key revogada |
| GET | /healthz | Status de saúde da API |
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ção | Admin | Analista | API Key |
|---|---|---|---|
| Visualizar dashboard e KPIs | Sim | Sim | Não |
| Consultar scores | Sim | Sim | Sim |
| Enviar eventos (ingestão) | Sim | Não | Sim |
| Gerenciar equipe | Sim | Não | Não |
| Gerenciar API keys | Sim | Não | Não |
| Remover membros desativados | Sim | Não | Não |
| Remover API keys revogadas | Sim | Não | Não |
Escopo de marca
| Identidade | Escopo | Comportamento |
|---|---|---|
| Admin (todas as marcas) | Todas as marcas da operadora | Vê e age em qualquer marca. Pode delegar acesso parcial a outros usuários. |
| Admin (parcial) | Lista explícita de marcas | Só 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 operadora | Lê dados de qualquer marca. Não pode mudar configurações. |
| Analista (parcial) | Lista explícita de marcas | Lê dados apenas das marcas atribuídas. |
| API Key | Sempre uma única marca | Eventos enviados com a key são atribuídos à marca dela. Consultas de score retornam apenas dados dessa marca. |
O perfil Analista tem acesso somente leitura ao dashboard e scores. Não pode enviar eventos, gerenciar equipe ou API keys.
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.
| Mecanismo | Descriçã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 marca | Cada 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 operadora | Admins e analistas pertencem a uma operadora. Visualizam apenas dados da operadora e, dentro dela, apenas das marcas a que têm acesso. |
| Benchmark anônimo | O ú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ódigo | Significado | Descrição |
|---|---|---|
400 | Bad Request | Corpo da requisição inválido ou campos obrigatórios ausentes |
401 | Unauthorized | API Key ou token JWT ausente ou inválido |
403 | Forbidden | Sem permissão para o recurso solicitado |
404 | Not Found | Recurso não encontrado |
409 | Conflict | Recurso duplicado (email já cadastrado, nome de chave já existe) |
410 | Gone | Link de ativação expirado |
422 | Unprocessable | Dados sintaticamente corretos mas semanticamente inválidos |
429 | Too Many Requests | Rate limit excedido |
500 | Internal Error | Erro 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.
| Endpoint | Limite padrão | Janela |
|---|---|---|
POST /ingest | 3.000 req/min | Token bucket, por chave |
GET /scores/changes | 60 req/min | Token bucket, por chave |
POST /auth/login | 5 tentativas / 15 min | Por email |
GET/POST /auth/setup | 10 tentativas/min | Por 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.
Respostas do endpoint de ingestão incluem X-RateLimit-Limit, X-RateLimit-Remaining e X-RateLimit-Reset para monitoramento proativo.