Sicurezza API
L'API di Buluthat gestisce il Suo centralino telefonico: avvia chiamate, chiude chiamate, invia SMS, elabora i numeri dei clienti. Per questo la fuga della chiave non significa "si vede un report" ma "si effettuano chiamate dal Suo account". Questa pagina spiega come proteggere la chiave e quali protezioni Buluthat applica per Suo conto.
Indirizzo base: https://api.buluthat.com/api/
I livelli a colpo d'occhio
Ogni richiesta supera in sequenza questi controlli. Se uno la rifiuta, la richiesta non viene elaborata e viene scritta nel registro.
| Ordine | Controllo | Cosa fa | Errore |
|---|---|---|---|
| 1 | Blocco anti brute force | Se un IP tenta 20 volte in 10 minuti con chiave o firma errata, quell'IP viene bloccato temporaneamente | 429 ip_locked |
| 2 | Limite del corpo | Un corpo della richiesta superiore a 5 MB non viene letto | 413 payload_too_large |
| 3 | Chiave | La chiave è accettata solo nell'intestazione; nel sistema viene conservato solo l'hash SHA-256 | 401 invalid_token |
| 4 | Durata e annullamento | Una chiave scaduta o revocata viene rifiutata | 401 token_expired |
| 5 | IP autorizzato | Se la chiave ha un elenco di IP, passano solo le richieste provenienti da tali indirizzi | 403 ip_not_allowed |
| 6 | Stato dell'account | Le chiavi di un account chiuso o disattivato non funzionano | 403 account_inactive |
| 7 | Ambito | La chiave accede solo alle API consentite | 403 scope_denied |
| 8 | Firma | Se nella chiave è attivo "firma obbligatoria", vengono verificati la firma HMAC, il timestamp e il nonce monouso | 401 signature_* |
| 9 | Limite di frequenza | Limite al minuto per chiave e sul totale dell'account | 429 rate_limited |
Guida rapida
- Nel pannello Account e Assistenza > Chiavi API aprire la pagina (la vede solo il referente dell'account).
- Nuova chiave: assegnate un nome, selezionate solo le autorizzazioni necessarie, indicate l'IP di uscita del Suo server, Richiesta firmata obbligatoria; aprirlo.
- Copiate i due valori mostrati una sola volta a schermo: chiave API (
bt_…, a ogni richiestaAuthorizationva nell'intestazione) e segreto di firma (bts_…, serve a firmare la richiesta, non viene mai inviato). - Provi la connessione:
curl "https://api.buluthat.com/api/autocall.php?action=ping" \
-H "Authorization: Bearer bt_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
Se la firma è obbligatoria, questa richiesta 401 signature_required restituisce; sotto Firma delle richieste utilizzi uno degli esempi della sezione.
Chiave e segreto di firma non sono leggibili nel sistema in forma non viene conservato. Se la perde non possiamo recuperarla; la rigenera dal pannello.
Ambiti
Ogni chiave viene generata con uno o più ambiti. La richiesta a un'API fuori dall'ambito 403 scope_denied restituisce.
| Ambito | Le API che apre |
|---|---|
autocall | Chiamata automatica (api/autocall.php). Per compatibilità con le integrazioni esistenti apre anche gli endpoint dell'assistente vocale, della verifica vocale e del controllo chiamate |
call | Controllo chiamate e code: begin_call, bridge, hangup, transfer, mute, queues, queues_pending, queue_user_list, queue_manage_users, agent_statuses |
voicebot | Assistente vocale (api/voicebot_api.php) e verifica vocale |
voice_otp | Codice di verifica vocale (api/voice_otp.php) |
sms | API SMS (api/sms.php). Nessun altro ambito può accedere agli SMS |
bridge | Dati del centralino in tempo reale (api/crm_bridge.php): chiamate in corso, stato degli operatori, registrazioni delle chiamate, registrazione audio, lista nera, annunci. Solo l'account della chiave; inviati tenant_id viene ignorato |
* | Tutte le API. Solo se davvero necessario |
Principio: una chiave per ogni integrazione, a ogni chiave il minimo delle autorizzazioni. Se la chiave SMS del Suo sito e-commerce trapela, l'aggressore non può avviare chiamate; revocate solo quella chiave.
Non inviare la chiave
Chiave nell'intestazione viene inviato:
Authorization: Bearer bt_xxxxxxxx
Authorization per gli ambienti che non possono impostare l'intestazione X-Api-Key: bt_xxxxxxxx è accettato anche.
Chiave nell'URL (?key=)
?key=bt_… il formato nelle nuove chiavi è disattivato e 401 query_key_disabled restituisce. Gli URL finiscono nei log del server web, nei registri dei proxy, nella cronologia del browser e Referer finisce nell'intestazione; la chiave trapela da lì. Solo per un vecchio sistema che non può inviare intestazioni, nelle impostazioni della chiave "Accetta la chiave nell'URL" può essere aperta. Il pannello evidenzia queste chiavi in rosso ?key= lo mostra con il badge.
Per le chiavi generate prima di V54 questo permesso è stato lasciato attivo per non interrompere le vecchie integrazioni. Lo disattivi dopo aver spostato la Sua integrazione sull'intestazione.
IP autorizzato
Nelle impostazioni della chiave Indirizzi IP autorizzati nel campo si scrive un IP o un blocco CIDR per riga:
85.111.89.220
10.20.0.0/24
2a01:4f8:c0c:1234::/64
Se l'elenco è vuoto, ogni IP è accettato. Se è compilato, la richiesta proveniente da indirizzi non in elenco, anche con chiave corretta, 403 ip_not_allowed restituisce. L'indirizzo da scrivere è quello che chiama l'API è l'IP di uscita del Suo server (non del Suo computer). Se non è sicuro, controlli nel registro la colonna IP della richiesta proveniente da quel server.
Firma delle richieste
La firma fa sì che non si possa inviare richieste neppure se la chiave viene sottratta: all'aggressore serve anche il segreto di firma, che non viaggia in rete in nessuna richiesta. La firma inoltre:
- Vincola il corpo: se nel percorso cambia anche un solo carattere la firma non corrisponde.
- Impedisce la riproduzione ripetuta: ogni nonce è accettato una sola volta; una richiesta intercettata non può essere inviata una seconda volta.
- Rifiuta la richiesta obsoleta: se il timestamp si discosta dall'ora del server di oltre ±5 minuti la richiesta viene rifiutata.
Nella chiave Richiesta firmata obbligatoria se attivo, ogni richiesta deve essere firmata. Anche se disattivo, se inviate le intestazioni di firma la firma viene comunque verificata; una firma errata non passa in silenzio.
Intestazioni
| Intestazione | Valore |
|---|---|
Authorization | Bearer bt_… |
X-Bt-Timestamp | Tempo Unix, secondi (es. 1790802088) |
X-Bt-Nonce | Nuovo a ogni richiesta, 16-64 caratteri A-Z a-z 0-9 _ - (es. 32 hex) |
X-Bt-Signature | v1= + la firma in esadecimale minuscolo |
Testo canonico
Il testo firmato, tra loro \n (LF) sono le sei righe:
v1
POST
/api/sms.php?action=send
1790802088
9f86d081884c7d659a2feaa0c55ad015
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
| Riga | Contenuto |
|---|---|
| 1 | Versione, fisso v1 |
| 2 | Metodo HTTP, in maiuscolo (GET, POST) |
| 3 | Percorso e stringa di query, così come è stata inviata la richiesta (/api/sms.php?action=send). Nome di dominio e schema non inclusi |
| 4 | X-Bt-Timestamp il valore |
| 5 | X-Bt-Nonce il valore |
| 6 | Hash SHA-256 del corpo, in esadecimale minuscolo. Nella richiesta senza corpo e multipart/form-data (per le richieste di caricamento file) l'hash del testo vuoto: e3b0c442…b855 |
Firma: 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'],
]));
Anche i client già pronti firmano: buluthat-autocall-client.php e buluthat-voice-otp-client.php nel quarto parametro ['signing_secret' => 'bts_…'] riceve.
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"}]}))
Riga di comando (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"
Perché la firma non corrisponde?
| Sintomo | Motivo |
|---|---|
signature_invalid solo nel POST | Il corpo firmato e il corpo inviato sono diversi. Il JSON una volta generate, passate la stessa variabile sia al riepilogo sia alla richiesta |
signature_invalid Nella ricerca con caratteri turchi | Ha composto voi stessi il percorso codificandolo in modo diverso. Nella firma usate il percorso e la query dell'URL realmente inviato dal client (parse_url / new URL()) |
signature_expired | L'orologio del Suo server è sfasato. NTP (timedatectl set-ntp true) aprire; tolleranza ±300 sec |
signature_replayed | Lo stesso nonce è stato inviato due volte. In caso di nuovo tentativo (retry) rigeneri nonce e timestamp producete di nuovo e firmate di nuovo |
signature_required | La chiave richiede la firma ma mancano le intestazioni |
signature_not_configured | La chiave non ha un segreto di firma; generate un "Nuovo segreto di firma" dal pannello |
Codici di errore
Gli errori di identità e sicurezza sono gli stessi su tutti gli endpoint JSON code restituisce i valori. Gli endpoint di controllo chiamate e code (compatibili con Verimor) restituiscono lo stesso codice HTTP con un messaggio in testo semplice.
| HTTP | code | Cosa fare |
|---|---|---|
| 401 | missing_token | Authorization: Bearer … aggiunga l'intestazione |
| 401 | invalid_token | La chiave è errata, revocata oppure assente. Non riprovare, correggere l'impostazione |
| 401 | token_expired | Rigeneri la chiave dal pannello |
| 401 | query_key_disabled | Inviate la chiave nell'intestazione anziché nell'URL |
| 401 | signature_* | Vedere la tabella sopra |
| 403 | ip_not_allowed | Aggiunga l'IP di uscita del Suo server all'elenco della chiave |
| 403 | scope_denied | Assegnate alla chiave l'autorizzazione necessaria oppure usate la chiave corretta |
| 403 | account_inactive | Account chiuso; contatti l'assistenza |
| 403 | module_disabled | Il servizio non è attivo nel Suo pacchetto |
| 413 | payload_too_large | Dividete la richiesta in parti (es. add_leads al massimo 5.000 record) |
| 429 | rate_limited / tenant_rate_limited | Retry-After attenda fino a |
| 429 | ip_locked | Da questo IP sono arrivati molti tentativi errati; correggete la configurazione errata, il blocco si rimuove da solo |
Esempio di risposta di errore:
{ "ok": false, "error": "Bu anahtarın bu API için yetkisi yok.", "code": "scope_denied", "request_id": "df5fca3a599653f6" }
Limiti di frequenza
| Limite | Predefinito |
|---|---|
| Per chiave | 120 richieste al minuto (riducibile dalle impostazioni della chiave) |
| Somma di tutte le chiavi dell'account | 600 richieste al minuto |
Stati degli operatori (agent_statuses) | inoltre 2 richieste al minuto per account |
In ogni risposta positiva le richieste residue arrivano nelle intestazioni:
X-RateLimit-Limit: 120
X-RateLimit-Remaining: 87
X-RateLimit-Reset: 1790802120
429 quando riceve Retry-After (secondi). Errore di rete e 5xx per 1 sec, 2 sec, 4 sec… al massimo 5 tentativi) usate un backoff esponenziale. 401/403 gli errori non riprovare: è un errore di configurazione, i tentativi attivano il blocco anti brute force.
Rotazione della chiave
Ruotate le chiavi ogni 90-180 giorni, quando un collaboratore lascia l'azienda o in caso di sospetta fuga. Passaggio senza interruzioni:
- Chiavi API > chiave interessata > Impostazioni > Rigenera la chiave. Scegliete "La vecchia resta attiva per 24 ore".
- Con le stesse impostazioni vengono generati una nuova chiave e un nuovo segreto di firma, mostrati una sola volta a schermo.
- Aggiornate la Vostra integrazione con i nuovi valori.
- Nel registro il prefisso della vecchia chiave (
bt_7820d84…) non è più visibile, attenda: allo scadere del tempo la vecchia chiave si chiude da sola.
In caso di sospetta fuga Scegliete "Chiudi subito la vecchia": le richieste con la vecchia chiave vengono rifiutate all'istante.
Registro delle richieste
Sotto la pagina Chiavi API Registro delle richieste mostra ogni richiesta: ora, prefisso della chiave, IP, endpoint e azione, stato HTTP, codice di errore, durata, firmata o no. Il riepilogo delle ultime 24 ore (richieste, errori, limiti di frequenza, errori di identità, richieste firmate, numero di IP diversi) è in cima alla pagina. I registri sono conservati 90 giorni.
In ogni risposta X-Request-Id c'è l'intestazione. Indichi questo valore nella richiesta di assistenza; troviamo subito la Sua richiesta nel registro. Non inserite mai la chiave o il segreto di firma in richieste di assistenza, e-mail o screenshot.
Se vede una richiesta da un IP che non riconosce o in orari inattesi, rigeneri subito la chiave con "Chiudi subito la vecchia".
Configurazione del CRM Byfix
Il CRM Byfix si collega a Buluthat con due identità distinte:
| Impostazione (CRM) | Valore |
|---|---|
| Impostazioni > Chiamata automatica > Chiave API | Generato in Buluthat bt_… la chiave (ambito: autocall + voicebot + voice_otp + call) |
| Impostazioni > Chiamata automatica > Segreto di firma | Della stessa chiave bts_… il segreto. Se compilato, ogni richiesta che il CRM invia a Buluthat viene firmata |
| Impostazioni VoIP > Buluthat API Token | Schermata in diretta / ponte CDR (crm_bridge); fornito dal team Buluthat, distinto dalla chiave sopra |
Ordine consigliato: la chiave Richiesta firmata obbligatoria generate disattivato, inserite chiave e segreto nel CRM, nel registro le richieste imzalı con il badge, poi rendete obbligatoria la firma nella chiave. Aggiungete alla chiave anche l'IP del server CRM.
Click-to-call (begin_call) ora invia la chiave nell'intestazione e non più nell'URL. Dopo l'aggiornamento del CRM può disattivare il permesso "Chiave nell'URL" sulla vecchia chiave.
Configurazione di ByCRM
In ByCRM ogni azienda a Buluthat con la propria chiave è collegata; le aziende non possono vedere i dati le une delle altre.
- Nel pannello Buluthat con l'account dell'azienda Chiavi API > Nuova chiave: autorizzazioni Dati del centralino in tempo reale, Controllo chiamate e code, Chiamata automatica (se utilizzato Assistente vocale, Codice di verifica vocale). Indichi l'IP del server ByCRM.
- ByCRM > Integrazioni > Buluthat:
| Campo | Valore |
|---|---|
| Buluthat API Token | bt_… |
| Segreto di firma | bts_… |
| Bridge Token | stesso bt_… la chiave |
| 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 | Non viene utilizzato; arrivano i dati dell'account a cui appartiene la chiave |
- Provate la schermata in diretta e il click-to-call, nel registro
imzalıvedete i badge, poi nella chiave Richiesta firmata obbligatoria; aprirlo.
Poiché la schermata in diretta interroga il ponte ogni pochi secondi, le richieste al ponte hanno un limite di frequenza separato e più ampio (1.200 al minuto per chiave); nel registro vengono scritte solo le richieste errate al ponte.
Verifica dei webhook
Anche i webhook che Buluthat Vi invia sono firmati (X-Buluthat-Signature: sha256=…). Sul Suo server la firma corpo grezzo non elaborate alcun webhook senza verifica tramite X-Buluthat-Delivery non elaborate due volte la stessa consegna con. Dettaglio: Webhook.
Lista di controllo della sicurezza
- [ ] Ogni integrazione ha la propria chiave, solo con gli ambiti necessari
- [ ] Le chiavi non sono nel codice ma in variabili d'ambiente o in una gestione dei segreti (
.envNon va su Git) - [ ] La chiave è solo lato server; non è nel JavaScript del browser, nell'app mobile, in una macro di Excel
- [ ] L'elenco degli IP autorizzati è compilato
- [ ] "Richiesta firmata obbligatoria" attivo
- [ ] Chiave nell'URL (
?key=) disattivato - [ ] Ha una data di scadenza oppure è impostato un promemoria di rinnovo nel calendario
- [ ] Il client verifica il certificato TLS (
CURLOPT_SSL_VERIFYPEERattivo); con la verifica disattivata, chi si inserisce nel mezzo può leggere e modificare la richiesta - [ ] L'orologio del server è sincronizzato con NTP
- [ ] La firma del webhook viene verificata
- [ ] Il registro viene rivisto una volta al mese; le chiavi accessibili al personale uscito sono state rigenerate
Segnalazione di vulnerabilità
Se ritiene di aver trovato una vulnerabilità nell'API di Buluthat, dal Centro Assistenza all'interno del pannello "Avviso di sicurezza" aprite una richiesta con oggetto. Spiegate come riprodurre la vulnerabilità e, se disponibile, X-Request-Id aggiunga i valori. La preghiamo di non condividere i dettagli finché non avremo esaminato e corretto la segnalazione.
