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.

OrdineControlloCosa faErrore
1Blocco anti brute forceSe un IP tenta 20 volte in 10 minuti con chiave o firma errata, quell'IP viene bloccato temporaneamente429 ip_locked
2Limite del corpoUn corpo della richiesta superiore a 5 MB non viene letto413 payload_too_large
3ChiaveLa chiave è accettata solo nell'intestazione; nel sistema viene conservato solo l'hash SHA-256401 invalid_token
4Durata e annullamentoUna chiave scaduta o revocata viene rifiutata401 token_expired
5IP autorizzatoSe la chiave ha un elenco di IP, passano solo le richieste provenienti da tali indirizzi403 ip_not_allowed
6Stato dell'accountLe chiavi di un account chiuso o disattivato non funzionano403 account_inactive
7AmbitoLa chiave accede solo alle API consentite403 scope_denied
8FirmaSe nella chiave è attivo "firma obbligatoria", vengono verificati la firma HMAC, il timestamp e il nonce monouso401 signature_*
9Limite di frequenzaLimite al minuto per chiave e sul totale dell'account429 rate_limited

Guida rapida

  1. Nel pannello Account e Assistenza > Chiavi API aprire la pagina (la vede solo il referente dell'account).
  2. Nuova chiave: assegnate un nome, selezionate solo le autorizzazioni necessarie, indicate l'IP di uscita del Suo server, Richiesta firmata obbligatoria; aprirlo.
  3. Copiate i due valori mostrati una sola volta a schermo: chiave API (bt_…, a ogni richiesta Authorization va nell'intestazione) e segreto di firma (bts_…, serve a firmare la richiesta, non viene mai inviato).
  4. 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.

AmbitoLe API che apre
autocallChiamata automatica (api/autocall.php). Per compatibilità con le integrazioni esistenti apre anche gli endpoint dell'assistente vocale, della verifica vocale e del controllo chiamate
callControllo chiamate e code: begin_call, bridge, hangup, transfer, mute, queues, queues_pending, queue_user_list, queue_manage_users, agent_statuses
voicebotAssistente vocale (api/voicebot_api.php) e verifica vocale
voice_otpCodice di verifica vocale (api/voice_otp.php)
smsAPI SMS (api/sms.php). Nessun altro ambito può accedere agli SMS
bridgeDati 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

IntestazioneValore
AuthorizationBearer bt_…
X-Bt-TimestampTempo Unix, secondi (es. 1790802088)
X-Bt-NonceNuovo a ogni richiesta, 16-64 caratteri A-Z a-z 0-9 _ - (es. 32 hex)
X-Bt-Signaturev1= + 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
RigaContenuto
1Versione, fisso v1
2Metodo HTTP, in maiuscolo (GET, POST)
3Percorso e stringa di query, così come è stata inviata la richiesta (/api/sms.php?action=send). Nome di dominio e schema non inclusi
4X-Bt-Timestamp il valore
5X-Bt-Nonce il valore
6Hash 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?

SintomoMotivo
signature_invalid solo nel POSTIl 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 turchiHa 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_expiredL'orologio del Suo server è sfasato. NTP (timedatectl set-ntp true) aprire; tolleranza ±300 sec
signature_replayedLo stesso nonce è stato inviato due volte. In caso di nuovo tentativo (retry) rigeneri nonce e timestamp producete di nuovo e firmate di nuovo
signature_requiredLa chiave richiede la firma ma mancano le intestazioni
signature_not_configuredLa 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.

HTTPcodeCosa fare
401missing_tokenAuthorization: Bearer … aggiunga l'intestazione
401invalid_tokenLa chiave è errata, revocata oppure assente. Non riprovare, correggere l'impostazione
401token_expiredRigeneri la chiave dal pannello
401query_key_disabledInviate la chiave nell'intestazione anziché nell'URL
401signature_*Vedere la tabella sopra
403ip_not_allowedAggiunga l'IP di uscita del Suo server all'elenco della chiave
403scope_deniedAssegnate alla chiave l'autorizzazione necessaria oppure usate la chiave corretta
403account_inactiveAccount chiuso; contatti l'assistenza
403module_disabledIl servizio non è attivo nel Suo pacchetto
413payload_too_largeDividete la richiesta in parti (es. add_leads al massimo 5.000 record)
429rate_limited / tenant_rate_limitedRetry-After attenda fino a
429ip_lockedDa 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

LimitePredefinito
Per chiave120 richieste al minuto (riducibile dalle impostazioni della chiave)
Somma di tutte le chiavi dell'account600 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:

  1. Chiavi API > chiave interessata > Impostazioni > Rigenera la chiave. Scegliete "La vecchia resta attiva per 24 ore".
  2. Con le stesse impostazioni vengono generati una nuova chiave e un nuovo segreto di firma, mostrati una sola volta a schermo.
  3. Aggiornate la Vostra integrazione con i nuovi valori.
  4. 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 APIGenerato in Buluthat bt_… la chiave (ambito: autocall + voicebot + voice_otp + call)
Impostazioni > Chiamata automatica > Segreto di firmaDella stessa chiave bts_… il segreto. Se compilato, ogni richiesta che il CRM invia a Buluthat viene firmata
Impostazioni VoIP > Buluthat API TokenSchermata 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.

  1. 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.
  2. ByCRM > Integrazioni > Buluthat:
CampoValore
Buluthat API Tokenbt_…
Segreto di firmabts_…
Bridge Tokenstesso bt_… la chiave
CRM Bridge URLhttps://api.buluthat.com/api/crm_bridge.php
Autocall URLhttps://api.buluthat.com/api/autocall.php
Buluthat Tenant ID / PBX Server IDNon viene utilizzato; arrivano i dati dell'account a cui appartiene la chiave
  1. 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 (.env Non 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_VERIFYPEER attivo); 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.