Segurança da API

A API da Buluthat gere a sua central telefónica: inicia chamadas, termina chamadas, envia SMS e processa números de clientes. Por isso, a fuga de uma chave não significa "ver-se um relatório", mas sim "fazerem-se chamadas a partir da sua conta". Esta página explica como proteger a sua chave e que proteções a Buluthat aplica em seu nome.

Endereço base: https://api.buluthat.com/api/

As camadas num relance

Cada pedido passa pelas seguintes portas, por ordem. Se uma o recusar, o pedido não é processado e fica registado.

OrdemPortaO que fazErro
1Bloqueio por força brutaSe um IP errar a chave ou a assinatura 20 vezes em 10 minutos, esse IP é bloqueado temporariamente429 ip_locked
2Limite do corpoNão é lido um corpo de pedido superior a 5 MB413 payload_too_large
3ChaveA chave é aceite apenas no cabeçalho; no sistema só é guardado o resumo SHA-256401 invalid_token
4Duração e cancelamentoÉ recusada uma chave expirada ou revogada401 token_expired
5IP autorizadoSe a chave tiver uma lista de IP definida, só passam os pedidos desses endereços403 ip_not_allowed
6Estado da contaAs chaves de uma conta encerrada ou inativa não funcionam403 account_inactive
7ÂmbitoA chave só acede às APIs autorizadas403 scope_denied
8AssinaturaSe a chave tiver "pedido assinado obrigatório" ativo, são validados a assinatura HMAC, o carimbo de data/hora e o nonce de utilização única401 signature_*
9Limite de pedidosLimite por minuto, por chave e no total da conta429 rate_limited

Início rápido

  1. No painel Conta e Apoio > Chaves de API abra a página (apenas o responsável da conta a vê).
  2. Nova chave: dê-lhe um nome, assinale apenas as permissões necessárias, indique o IP de saída do seu servidor, Pedido assinado obrigatórioAbra
  3. Copie os dois valores mostrados uma única vez no ecrã: chave de API (bt_…, em cada pedido Authorization vai no cabeçalho) e segredo de assinatura (bts_…, é utilizado para assinar o pedido, nunca é enviado).
  4. Experimente a ligação:
curl "https://api.buluthat.com/api/autocall.php?action=ping" \
  -H "Authorization: Bearer bt_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

Se a assinatura for obrigatória, este pedido 401 signature_required devolve; o seguinte Assinatura de pedidos use um dos exemplos da secção.

A chave e o segredo de assinatura não ficam no sistema de forma legível não é guardado. Se a perder, não a conseguimos recuperar; renove a chave no painel.

Âmbitos

Cada chave é gerada com um ou mais âmbitos. Um pedido a uma API fora do âmbito 403 scope_denied devolve.

ÂmbitoAPIs que abre
autocallChamadas Automáticas (api/autocall.php). Para compatibilidade com integrações antigas, abre também os endpoints do assistente de voz, da verificação por voz e do controlo de chamadas
callControlo de chamadas e filas: begin_call, bridge, hangup, transfer, mute, queues, queues_pending, queue_user_list, queue_manage_users, agent_statuses
voicebotAssistente de Voz (api/voicebot_api.php) e verificação por voz
voice_otpCódigo de Verificação por Voz (api/voice_otp.php)
smsAPI de SMS (api/sms.php). Nenhum outro âmbito acede ao SMS
bridgeDados em direto da central (api/crm_bridge.php): chamadas em direto, estado dos agentes, gravações de chamadas, gravação de áudio, lista negra, anúncios. Apenas da conta da chave; os enviados tenant_id é ignorado
*Todas as APIs. Apenas se for realmente necessário

Princípio: uma chave por integração, o mínimo de permissões por chave. Se a chave de SMS do seu site de comércio eletrónico vazar, o atacante não consegue iniciar chamadas; revoga apenas essa chave.

Não enviar a chave

Chave no cabeçalho é enviado:

Authorization: Bearer bt_xxxxxxxx

Authorization para ambientes que não conseguem definir o cabeçalho X-Api-Key: bt_xxxxxxxx também é aceite.

Chave no URL (?key=)

?key=bt_… o formato, nas chaves novas está desativado e 401 query_key_disabled devolve. Os URL ficam nos registos do servidor web, nos registos de proxies, no histórico do navegador e Referer cai no cabeçalho; a chave fuga por aí. Apenas para um sistema antigo que não consiga enviar cabeçalhos, nas definições da chave "Aceitar a chave no URL" pode ser aberto. O painel mostra estas chaves a vermelho ?key= mostra com o selo.

Nas chaves geradas antes da V54, esta permissão foi deixada ativa para não quebrar as integrações antigas. Desative-a depois de migrar a sua integração para o cabeçalho.

IP autorizado

Nas definições da chave Endereços IP autorizados no campo escreve-se um IP ou bloco CIDR por linha:

85.111.89.220
10.20.0.0/24
2a01:4f8:c0c:1234::/64

Se a lista estiver vazia, qualquer IP é aceite. Se tiver conteúdo, um pedido de fora dos endereços da lista, mesmo com a chave correta, 403 ip_not_allowed devolve. O endereço a indicar é aquele de onde chama a API é o IP de saída do seu servidor (não do seu próprio computador). Em caso de dúvida, consulte no registo a coluna IP do pedido proveniente desse servidor.

Assinatura de pedidos

A assinatura garante que, mesmo com a chave comprometida, não é possível fazer pedidos: o atacante precisa também do segredo de assinatura, que nunca viaja pela rede em nenhum pedido. A assinatura também:

  • Bloqueia o corpo: se mudar um único carácter no caminho, a assinatura não coincide.
  • Impede a repetição: cada nonce é aceite uma só vez; um pedido intercetado não pode ser enviado uma segunda vez.
  • Recusa o pedido desatualizado: se o carimbo de data/hora se desviar mais de ±5 minutos da hora do servidor, o pedido é recusado.

Na chave Pedido assinado obrigatório se ativo, todos os pedidos têm de ser assinados. Mesmo que esteja desativado, se enviar os cabeçalhos de assinatura, a assinatura continua a ser validada; uma assinatura errada não passa em silêncio.

Cabeçalhos

CabeçalhoValor
AuthorizationBearer bt_…
X-Bt-TimestampHora Unix, em segundos (p. ex. 1790802088)
X-Bt-NonceNovo em cada pedido, 16-64 caracteres A-Z a-z 0-9 _ - (p. ex. 32 hex)
X-Bt-Signaturev1= + a assinatura em hexadecimal minúsculo

Texto canónico

O texto assinado, entre eles \n (LF) são seis linhas:

v1
POST
/api/sms.php?action=send
1790802088
9f86d081884c7d659a2feaa0c55ad015
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
LinhaConteúdo
1Versão, fixo v1
2Método HTTP, em maiúsculas (GET, POST)
3Caminho e query string, tal como o pedido foi enviado (/api/sms.php?action=send). Não inclui o nome de domínio nem o esquema
4X-Bt-Timestamp o valor
5X-Bt-Nonce o valor
6Resumo SHA-256 do corpo, em hexadecimal minúsculo. Num pedido sem corpo e multipart/form-data (carregamento de ficheiros) o resumo do corpo vazio: e3b0c442…b855

Assinatura: hex( HMAC-SHA256( anahtar = imza_sırrı, mesaj = kanonik_metin ) )

PHP

function buluthat_request(string $method, string $url, string $token, string $secret, ?array $data = null): array
{
    $body = $data === null ? '' : json_encode($data, JSON_UNESCAPED_UNICODE);
    $p = parse_url($url);
    $uri = ($p['path'] ?? '/') . (isset($p['query']) ? '?' . $p['query'] : '');
    $ts = (string)time();
    $nonce = bin2hex(random_bytes(16));
    $canonical = implode("\n", ['v1', strtoupper($method), $uri, $ts, $nonce, hash('sha256', $body)]);

    $ch = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_CUSTOMREQUEST  => $method,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT        => 30,
        CURLOPT_HTTPHEADER     => [
            'Authorization: Bearer ' . $token,
            'Content-Type: application/json',
            'X-Bt-Timestamp: ' . $ts,
            'X-Bt-Nonce: ' . $nonce,
            'X-Bt-Signature: v1=' . hash_hmac('sha256', $canonical, $secret),
        ],
    ]);
    if ($body !== '') {
        curl_setopt($ch, CURLOPT_POSTFIELDS, $body);   // imzalanan gövdenin AYNISI
    }
    $res = json_decode((string)curl_exec($ch), true) ?: [];
    curl_close($ch);
    return $res;
}

$token  = getenv('BULUTHAT_TOKEN');    // bt_...
$secret = getenv('BULUTHAT_SECRET');   // bts_...
print_r(buluthat_request('POST', 'https://api.buluthat.com/api/sms.php?action=send', $token, $secret, [
    'header' => 'FIRMAM', 'message' => 'Siparişiniz kargoya verildi.', 'phones' => ['05321234567'],
]));

Os clientes prontos também assinam: buluthat-autocall-client.php e buluthat-voice-otp-client.php no quarto parâmetro ['signing_secret' => 'bts_…'] recebe.

Node.js

const crypto = require('crypto');

async function buluthatRequest(method, url, token, secret, data) {
  const body = data === undefined ? '' : JSON.stringify(data);
  const u = new URL(url);
  const ts = Math.floor(Date.now() / 1000).toString();
  const nonce = crypto.randomBytes(16).toString('hex');
  const canonical = ['v1', method.toUpperCase(), u.pathname + u.search, ts, nonce,
    crypto.createHash('sha256').update(body).digest('hex')].join('\n');
  const signature = crypto.createHmac('sha256', secret).update(canonical).digest('hex');

  const res = await fetch(url, {
    method,
    headers: {
      'Authorization': `Bearer ${token}`,
      'Content-Type': 'application/json',
      'X-Bt-Timestamp': ts,
      'X-Bt-Nonce': nonce,
      'X-Bt-Signature': `v1=${signature}`,
    },
    body: body || undefined,
  });
  return res.json();
}

buluthatRequest('GET', 'https://api.buluthat.com/api/voice_otp.php?action=status&id=42',
  process.env.BULUTHAT_TOKEN, process.env.BULUTHAT_SECRET).then(console.log);

Python

import hashlib, hmac, json, os, secrets, time, urllib.parse
import requests

def buluthat_request(method, url, token, secret, data=None):
    body = b"" if data is None else json.dumps(data, ensure_ascii=False).encode("utf-8")
    u = urllib.parse.urlsplit(url)
    uri = u.path + ("?" + u.query if u.query else "")
    ts = str(int(time.time()))
    nonce = secrets.token_hex(16)
    canonical = "\n".join(["v1", method.upper(), uri, ts, nonce, hashlib.sha256(body).hexdigest()])
    sig = hmac.new(secret.encode(), canonical.encode("utf-8"), hashlib.sha256).hexdigest()
    headers = {
        "Authorization": f"Bearer {token}",
        "Content-Type": "application/json",
        "X-Bt-Timestamp": ts,
        "X-Bt-Nonce": nonce,
        "X-Bt-Signature": f"v1={sig}",
    }
    return requests.request(method, url, data=body or None, headers=headers, timeout=30).json()

print(buluthat_request("POST", "https://api.buluthat.com/api/autocall.php?action=add_leads",
      os.environ["BULUTHAT_TOKEN"], os.environ["BULUTHAT_SECRET"],
      {"campaign_id": 12, "leads": [{"phone": "05321234567", "name": "Ayşe Yılmaz"}]}))

Linha de comandos (bash + openssl)

TOKEN=bt_xxx; SECRET=bts_xxx
URI='/api/autocall.php?action=ping'
TS=$(date +%s); NONCE=$(openssl rand -hex 16)
BODYHASH=$(printf '' | openssl dgst -sha256 -hex | awk '{print $NF}')
SIG=$(printf 'v1\nGET\n%s\n%s\n%s\n%s' "$URI" "$TS" "$NONCE" "$BODYHASH" \
      | openssl dgst -sha256 -hmac "$SECRET" -hex | awk '{print $NF}')
curl "https://api.buluthat.com$URI" -H "Authorization: Bearer $TOKEN" \
  -H "X-Bt-Timestamp: $TS" -H "X-Bt-Nonce: $NONCE" -H "X-Bt-Signature: v1=$SIG"

Porque não coincide a assinatura?

SintomaMotivo
signature_invalid apenas no POSTO corpo que assinou é diferente do corpo que enviou. O JSON uma vez gere-a e dê a mesma variável ao resumo e ao pedido
signature_invalid Numa consulta com carateres turcosVocê compôs o caminho por si mesmo e codificou de forma diferente. Na assinatura, use o caminho e a query do URL que o cliente realmente envia (parse_url / new URL())
signature_expiredA hora do seu servidor está desfasada. NTP (timedatectl set-ntp true) abra; tolerância de ±300 s
signature_replayedO mesmo nonce foi enviado duas vezes. Numa nova tentativa (retry), o nonce e o carimbo de data/hora gere novamente e assine novamente
signature_requiredA chave exige assinatura, mas faltam os cabeçalhos
signature_not_configuredA chave não tem segredo de assinatura; gere um "Novo segredo de assinatura" no painel

Códigos de erro

Os erros de identidade e segurança são iguais em todos os endpoints JSON code devolve com os valores. Os endpoints de controlo de chamadas e de filas (compatíveis com Verimor) devolvem o mesmo código HTTP com mensagem em texto simples.

HTTPcodeO que fazer
401missing_tokenAuthorization: Bearer … acrescente o cabeçalho
401invalid_tokenChave incorreta, revogada ou inexistente. Não tentar de novo, corrija a definição
401token_expiredRenove a chave no painel
401query_key_disabledEnvie a chave no cabeçalho e não no URL
401signature_*Veja a tabela acima
403ip_not_allowedAdicione o IP de saída do seu servidor à lista da chave
403scope_deniedAtribua à chave a permissão correspondente ou utilize a chave certa
403account_inactiveConta desativada; contacte o apoio
403module_disabledO serviço em causa não está ativo no seu pacote
413payload_too_largeDivida o pedido em partes (p. ex. add_leads no máximo 5.000 registos)
429rate_limited / tenant_rate_limitedRetry-After aguarde até
429ip_lockedForam recebidas muitas tentativas falhadas deste IP; corrija a configuração errada e o bloqueio é levantado automaticamente

Exemplo de resposta de erro:

{ "ok": false, "error": "Bu anahtarın bu API için yetkisi yok.", "code": "scope_denied", "request_id": "df5fca3a599653f6" }

Limites de pedidos

LimitePredefinido
Por chave120 pedidos por minuto (pode ser reduzido nas definições da chave)
Soma de todas as chaves da conta600 pedidos por minuto
Estados dos agentes (agent_statuses)e, além disso, 2 pedidos por minuto por conta

Em cada resposta bem-sucedida, os limites restantes vêm nos cabeçalhos:

X-RateLimit-Limit: 120
X-RateLimit-Remaining: 87
X-RateLimit-Reset: 1790802120

429 ao receber Retry-After aguarde (segundos). Erro de rede e 5xx use recuo exponencial (1 s, 2 s, 4 s… no máximo 5 tentativas). 401/403 os erros não tentar de novo: é um erro de configuração; as tentativas ativam o bloqueio por força bruta.

Renovação de chave

Renove as chaves a cada 90-180 dias, quando um colaborador sair ou em caso de suspeita de fuga. Transição sem interrupção:

  1. Chaves de API > chave em causa > Definições > Renovar chave. Selecione "A anterior funciona durante 24 horas".
  2. É gerada uma nova chave e um novo segredo de assinatura com as mesmas definições, apresentados uma única vez no ecrã.
  3. Atualize a sua integração com os novos valores.
  4. No registo, o prefixo da chave antiga (bt_7820d84…) deixou de aparecer, aguarde; quando o prazo terminar, a chave antiga fecha-se automaticamente.

Em caso de suspeita de fuga Selecione "Fechar a anterior de imediato"; os pedidos com a chave antiga são recusados de imediato.

Registo de pedidos

Na parte inferior da página Chaves de API Registo de pedidos mostra cada pedido: hora, prefixo da chave, IP, endpoint e ação, estado HTTP, código de erro, duração, se foi assinado. O resumo das últimas 24 horas (pedidos, erros, limite de pedidos, erros de identidade, pedidos assinados, número de IP distintos) está no topo da página. Os registos são conservados 90 dias.

Em cada resposta X-Request-Id tem o cabeçalho. Indique este valor no pedido de apoio; encontramos de imediato o seu pedido no registo. Nunca coloque a chave ou o segredo de assinatura num pedido de apoio, num e-mail ou numa captura de ecrã.

Se vir um IP que não reconhece ou pedidos a horas inesperadas, renove de imediato a chave com "Fechar a anterior de imediato".

Configuração do Byfix CRM

O Byfix CRM liga-se à Buluthat com duas identidades distintas:

Definição (CRM)Valor
Definições > Chamadas Automáticas > Chave de APIGerado na Buluthat bt_… chave (âmbito: autocall + voicebot + voice_otp + call)
Definições > Chamadas Automáticas > Segredo de AssinaturaDa mesma chave bts_… segredo. Se estiver preenchido, cada pedido que o CRM envia à Buluthat é assinado
Definições de VoIP > Buluthat API TokenEcrã em direto / ponte CDR (crm_bridge); fornecida pela equipa Buluthat, é distinta da chave acima

Ordem recomendada: a chave Pedido assinado obrigatório gere-a desativada, indique a chave e o segredo no CRM e veja os pedidos no registo imzalı veja que chega com o selo, depois torne a assinatura obrigatória na chave. Adicione também à chave o IP do servidor do CRM.

Click-to-call (begin_call) passa a enviar a chave no cabeçalho e já não no URL. Depois de atualizar o CRM, pode desativar a permissão "Chave no URL" da chave antiga.

Configuração do ByCRM

No ByCRM, cada empresa liga-se à Buluthat com a sua própria chave é associado; as empresas não veem os dados umas das outras.

  1. No painel da Buluthat, com a conta da empresa Chaves de API > Nova chave: permissões Dados em Direto da Central, Controlo de Chamadas e Filas, Chamadas Automáticas (se for utilizado Assistente de Voz, Código de Verificação por Voz). Indique o IP do servidor do ByCRM.
  2. ByCRM > Integrações > Buluthat:
CampoValor
Buluthat API Tokenbt_…
Segredo de assinaturabts_…
Bridge Tokeno mesmo bt_… a chave
CRM Bridge URLhttps://api.buluthat.com/api/crm_bridge.php
Autocall URLhttps://api.buluthat.com/api/autocall.php
Buluthat Tenant ID / PBX Server IDNão é utilizado; vêm os dados da conta à qual a chave pertence
  1. Experimente o ecrã em direto e o click-to-call; no registo imzalı veja os selos, depois na chave Pedido assinado obrigatórioAbra
Como o ecrã em direto consulta a ponte de poucos em poucos segundos, os pedidos da ponte têm um limite próprio e mais alargado (1.200 por minuto, por chave); no registo apenas ficam os pedidos da ponte que falham.

Validação dos webhooks

Os webhooks que a Buluthat lhe envia também são assinados (X-Buluthat-Signature: sha256=…). No seu servidor, a assinatura corpo bruto não processe nenhum webhook sem validar através de; X-Buluthat-Delivery não processe duas vezes a mesma entrega que. Detalhes: Webhooks.

Lista de verificação de segurança

  • [ ] Cada integração tem a sua própria chave, apenas com os âmbitos necessários
  • [ ] As chaves não estão no código, mas em variáveis de ambiente ou gestão de segredos (.env Não vai para o Git)
  • [ ] A chave está apenas no lado do servidor; não está no JavaScript do navegador, na aplicação móvel nem em macros do Excel
  • [ ] A lista de IP autorizados está preenchida
  • [ ] Pedido assinado obrigatório ativo
  • [ ] Chave no URL (?key=) desativado
  • [ ] Tem data de expiração ou há um lembrete de renovação no calendário
  • [ ] O cliente valida o certificado TLS (CURLOPT_SSL_VERIFYPEER ativo); com a verificação desligada, quem se interpuser pode ler e alterar o pedido
  • [ ] A hora do servidor está sincronizada por NTP
  • [ ] A assinatura do webhook é validada
  • [ ] O registo é revisto uma vez por mês; as chaves a que tinha acesso pessoal que saiu foram renovadas

Comunicação de vulnerabilidade

Se considera ter encontrado uma vulnerabilidade na API da Buluthat, através do Centro de Apoio no painel "Aviso de segurança" abra um registo com o assunto. Indique como reproduziu a vulnerabilidade e, se houver, X-Request-Id acrescente os valores. Pedimos-lhe que não partilhe os detalhes até analisarmos e corrigirmos a comunicação.