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.
| Ordem | Porta | O que faz | Erro |
|---|---|---|---|
| 1 | Bloqueio por força bruta | Se um IP errar a chave ou a assinatura 20 vezes em 10 minutos, esse IP é bloqueado temporariamente | 429 ip_locked |
| 2 | Limite do corpo | Não é lido um corpo de pedido superior a 5 MB | 413 payload_too_large |
| 3 | Chave | A chave é aceite apenas no cabeçalho; no sistema só é guardado o resumo SHA-256 | 401 invalid_token |
| 4 | Duração e cancelamento | É recusada uma chave expirada ou revogada | 401 token_expired |
| 5 | IP autorizado | Se a chave tiver uma lista de IP definida, só passam os pedidos desses endereços | 403 ip_not_allowed |
| 6 | Estado da conta | As chaves de uma conta encerrada ou inativa não funcionam | 403 account_inactive |
| 7 | Âmbito | A chave só acede às APIs autorizadas | 403 scope_denied |
| 8 | Assinatura | Se 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 única | 401 signature_* |
| 9 | Limite de pedidos | Limite por minuto, por chave e no total da conta | 429 rate_limited |
Início rápido
- No painel Conta e Apoio > Chaves de API abra a página (apenas o responsável da conta a vê).
- 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
- Copie os dois valores mostrados uma única vez no ecrã: chave de API (
bt_…, em cada pedidoAuthorizationvai no cabeçalho) e segredo de assinatura (bts_…, é utilizado para assinar o pedido, nunca é enviado). - 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.
| Âmbito | APIs que abre |
|---|---|
autocall | Chamadas 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 |
call | Controlo de chamadas e filas: begin_call, bridge, hangup, transfer, mute, queues, queues_pending, queue_user_list, queue_manage_users, agent_statuses |
voicebot | Assistente de Voz (api/voicebot_api.php) e verificação por voz |
voice_otp | Código de Verificação por Voz (api/voice_otp.php) |
sms | API de SMS (api/sms.php). Nenhum outro âmbito acede ao SMS |
bridge | Dados 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çalho | Valor |
|---|---|
Authorization | Bearer bt_… |
X-Bt-Timestamp | Hora Unix, em segundos (p. ex. 1790802088) |
X-Bt-Nonce | Novo em cada pedido, 16-64 caracteres A-Z a-z 0-9 _ - (p. ex. 32 hex) |
X-Bt-Signature | v1= + 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
| Linha | Conteúdo |
|---|---|
| 1 | Versão, fixo v1 |
| 2 | Método HTTP, em maiúsculas (GET, POST) |
| 3 | Caminho 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 |
| 4 | X-Bt-Timestamp o valor |
| 5 | X-Bt-Nonce o valor |
| 6 | Resumo 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?
| Sintoma | Motivo |
|---|---|
signature_invalid apenas no POST | O 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 turcos | Você 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_expired | A hora do seu servidor está desfasada. NTP (timedatectl set-ntp true) abra; tolerância de ±300 s |
signature_replayed | O mesmo nonce foi enviado duas vezes. Numa nova tentativa (retry), o nonce e o carimbo de data/hora gere novamente e assine novamente |
signature_required | A chave exige assinatura, mas faltam os cabeçalhos |
signature_not_configured | A 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.
| HTTP | code | O que fazer |
|---|---|---|
| 401 | missing_token | Authorization: Bearer … acrescente o cabeçalho |
| 401 | invalid_token | Chave incorreta, revogada ou inexistente. Não tentar de novo, corrija a definição |
| 401 | token_expired | Renove a chave no painel |
| 401 | query_key_disabled | Envie a chave no cabeçalho e não no URL |
| 401 | signature_* | Veja a tabela acima |
| 403 | ip_not_allowed | Adicione o IP de saída do seu servidor à lista da chave |
| 403 | scope_denied | Atribua à chave a permissão correspondente ou utilize a chave certa |
| 403 | account_inactive | Conta desativada; contacte o apoio |
| 403 | module_disabled | O serviço em causa não está ativo no seu pacote |
| 413 | payload_too_large | Divida o pedido em partes (p. ex. add_leads no máximo 5.000 registos) |
| 429 | rate_limited / tenant_rate_limited | Retry-After aguarde até |
| 429 | ip_locked | Foram 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
| Limite | Predefinido |
|---|---|
| Por chave | 120 pedidos por minuto (pode ser reduzido nas definições da chave) |
| Soma de todas as chaves da conta | 600 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:
- Chaves de API > chave em causa > Definições > Renovar chave. Selecione "A anterior funciona durante 24 horas".
- É gerada uma nova chave e um novo segredo de assinatura com as mesmas definições, apresentados uma única vez no ecrã.
- Atualize a sua integração com os novos valores.
- 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 API | Gerado na Buluthat bt_… chave (âmbito: autocall + voicebot + voice_otp + call) |
| Definições > Chamadas Automáticas > Segredo de Assinatura | Da mesma chave bts_… segredo. Se estiver preenchido, cada pedido que o CRM envia à Buluthat é assinado |
| Definições de VoIP > Buluthat API Token | Ecrã 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.
- 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.
- ByCRM > Integrações > Buluthat:
| Campo | Valor |
|---|---|
| Buluthat API Token | bt_… |
| Segredo de assinatura | bts_… |
| Bridge Token | o mesmo bt_… a chave |
| CRM Bridge URL | https://api.buluthat.com/api/crm_bridge.php |
| Autocall URL | https://api.buluthat.com/api/autocall.php |
| Buluthat Tenant ID / PBX Server ID | Não é utilizado; vêm os dados da conta à qual a chave pertence |
- 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 (
.envNã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_VERIFYPEERativo); 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.
