Bezpieczeństwo API

API Buluthat zarządza Twoją centralą telefoniczną: inicjuje połączenia, kończy rozmowy, wysyła SMS-y, przetwarza numery klientów. Dlatego wyciek klucza nie oznacza „widoczny raport”, lecz „połączenia wykonywane z Twojego konta”. Ta strona wyjaśnia, jak chronić klucz i jakie zabezpieczenia Buluthat stosuje w Twoim imieniu.

Adres bazowy: https://api.buluthat.com/api/

Warstwy w skrócie

Każde żądanie przechodzi po kolei przez te bramki. Jeśli któraś odrzuci, żądanie nie jest przetwarzane i trafia do dziennika.

KolejnośćBramkaCo robiBłąd
1Blokada brute forceJeśli z jednego IP w ciągu 10 minut 20 razy zostanie użyty błędny klucz lub podpis, adres IP zostaje tymczasowo zablokowany429 ip_locked
2Limit treściTreść żądania większa niż 5 MB nie jest odczytywana413 payload_too_large
3KluczKlucz jest akceptowany wyłącznie w nagłówku; w systemie przechowywany jest tylko skrót SHA-256401 invalid_token
4Czas i anulowanieKlucz, który wygasł lub został unieważniony, jest odrzucany401 token_expired
5Dozwolone IPJeśli dla klucza zdefiniowano listę IP, przechodzą tylko żądania z tych adresów403 ip_not_allowed
6Status kontaKlucze zamkniętego lub nieaktywnego konta nie działają403 account_inactive
7ZakresKlucz ma dostęp wyłącznie do dozwolonych API403 scope_denied
8PodpisJeśli w kluczu włączono „wymagaj podpisanego żądania”, weryfikowany jest podpis HMAC, znacznik czasu i jednorazowy nonce401 signature_*
9Limit zapytańLimit na minutę na klucz i łącznie na konto429 rate_limited

Szybki start

  1. W panelu Konto i wsparcie > Klucze API otwórz stronę (widzi ją tylko osoba upoważniona konta).
  2. Nowy klucz: nadaj nazwę, zaznacz tylko wymagane uprawnienia, wpisz adres IP wychodzący Twojego serwera, Podpisane żądanie wymaganeOtwórz
  3. Skopiuj dwie wartości wyświetlone na ekranie jednorazowo: klucz API (bt_…, w każdym żądaniu Authorization idzie w nagłówku) oraz sekret podpisu (bts_…, służy do podpisywania żądania, nigdy nie jest wysyłane).
  4. Wypróbuj połączenie:
curl "https://api.buluthat.com/api/autocall.php?action=ping" \
  -H "Authorization: Bearer bt_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

Jeśli podpis jest obowiązkowy, to żądanie 401 signature_required zwraca; poniższe Podpisywanie żądań użyj jednego z przykładów w sekcji.

Klucz i sekret podpisu nie są w systemie przechowywane w postaci możliwej do odczytania nie jest przechowywane. W razie utraty nie odzyskamy go; klucz odnawiasz w panelu.

Zakresy

Każdy klucz jest tworzony z jednym lub kilkoma zakresami. Żądanie do API spoza zakresu 403 scope_denied zwraca.

ZakresAPI, które otwiera
autocallAutomatyczne połączenia (api/autocall.php). Dla zgodności ze starszymi integracjami otwiera także punkty końcowe asystenta głosowego, głosowej weryfikacji i sterowania połączeniami
callSterowanie połączeniami i kolejki: begin_call, bridge, hangup, transfer, mute, queues, queues_pending, queue_user_list, queue_manage_users, agent_statuses
voicebotAsystent Głosowy (api/voicebot_api.php) i głosową weryfikację
voice_otpGłosowy kod weryfikacyjny (api/voice_otp.php)
smsAPI SMS (api/sms.php). Żaden inny zakres nie ma dostępu do SMS
bridgeDane centrali na żywo (api/crm_bridge.php): połączenia na żywo, status konsultantów, nagrania połączeń, nagrania rozmów, czarna lista, komunikaty. Wyłącznie konto klucza; wysłane tenant_id jest ignorowane
*Wszystkie API. Tylko jeśli naprawdę potrzebne

Zasada: oddzielny klucz dla każdej integracji, minimum uprawnień dla każdego klucza. Jeśli klucz SMS Twojego sklepu internetowego wycieknie, napastnik nie może inicjować połączeń; unieważniasz tylko ten klucz.

Nie wysyłaj klucza

Klucz w nagłówku jest wysyłane:

Authorization: Bearer bt_xxxxxxxx

Authorization dla środowisk, które nie mogą ustawić nagłówka X-Api-Key: bt_xxxxxxxx także jest akceptowane.

Klucz w adresie URL (?key=)

?key=bt_… format w nowych kluczach jest wyłączone i 401 query_key_disabled zwraca. Adresy URL trafiają do logów serwera WWW, rejestrów proxy, historii przeglądarki i Referer trafia do nagłówka; klucz stamtąd wycieka. Tylko dla starszego systemu, który nie może wysłać nagłówka, w ustawieniach klucza „Akceptuj klucz w adresie URL” można otworzyć. Panel oznacza te klucze na czerwono ?key= pokazuje z odznaką.

W kluczach wygenerowanych przed V54 to uprawnienie zostało pozostawione włączone, aby nie zerwać starszych integracji. Wyłącz je po przeniesieniu integracji do nagłówka.

Dozwolone IP

W ustawieniach klucza Dozwolone adresy IP w polu wpisuje się po jednym IP lub bloku CIDR w wierszu:

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

Jeśli lista jest pusta, akceptowany jest każdy adres IP. Jeśli jest wypełniona, żądanie spoza adresów z listy jest odrzucane, nawet gdy klucz jest poprawny 403 ip_not_allowed zwraca. Adres do wpisania to ten, który wywołuje API to adres IP wychodzący Twojego serwera (a nie Twojego komputera). Jeśli nie masz pewności, sprawdź w dzienniku kolumnę IP żądania pochodzącego z tego serwera.

Podpisywanie żądań

Podpis sprawia, że nawet po przejęciu klucza nie można wysyłać żądań: napastnik potrzebuje dodatkowo sekretu podpisu, który nie jest przesyłany siecią w żadnym żądaniu. Podpis jednocześnie:

  • Blokuje treść: jeśli zmieni się choć jeden znak w ścieżce, podpis się nie zgodzi.
  • Zapobiega ponownemu odtworzeniu: każdy nonce jest akceptowany tylko raz; przechwycone żądanie nie może zostać wysłane ponownie.
  • Odrzuca przeterminowane żądanie: żądanie jest odrzucane, jeśli znacznik czasu odbiega od czasu serwera o więcej niż ±5 minut.

W kluczu Podpisane żądanie wymagane gdy włączone, każde żądanie musi być podpisane. Nawet gdy wyłączone, jeśli wyślesz nagłówki podpisu, podpis i tak jest weryfikowany; błędny podpis nie przejdzie bezszelestnie.

Nagłówki

NagłówekWartość
AuthorizationBearer bt_…
X-Bt-TimestampCzas Unix, w sekundach (np. 1790802088)
X-Bt-NoncePrzy każdym żądaniu nowy, 16-64 znaków A-Z a-z 0-9 _ - (np. 32 hex)
X-Bt-Signaturev1= + podpis zapisany małymi literami hex

Tekst kanoniczny

Podpisywany tekst, między nimi \n (LF) to sześć wierszy:

v1
POST
/api/sms.php?action=send
1790802088
9f86d081884c7d659a2feaa0c55ad015
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
WierszTreść
1Wersja, stała v1
2Metoda HTTP, wielkimi literami (GET, POST)
3Ścieżka i ciąg zapytania, w postaci, w jakiej żądanie zostało wysłane (/api/sms.php?action=send). Nazwa domeny i schemat nie są wliczone
4X-Bt-Timestamp wartość
5X-Bt-Nonce wartość
6Skrót SHA-256 treści, małymi literami hex. Przy żądaniu bez treści i multipart/form-data (przesyłanie plików) — skrót pustego tekstu: e3b0c442…b855

Podpis: 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'],
]));

Gotowi klienci również podpisują: buluthat-autocall-client.php i buluthat-voice-otp-client.php w czwartym parametrze ['signing_secret' => 'bts_…'] otrzymuje.

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"}]}))

Wiersz poleceń (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"

Dlaczego podpis się nie zgadza?

ObjawPrzyczyna
signature_invalid tylko w POSTTreść, którą podpisałeś, różni się od treści, którą wysłałeś. JSON jednorazowo wygeneruj, tę samą zmienną podaj zarówno w podsumowaniu, jak i w żądaniu
signature_invalid W zapytaniu z tureckimi znakamiZłożyłeś ścieżkę samodzielnie i zakodowałeś inaczej. W podpisie użyj ścieżki i zapytania z adresu URL, który klient faktycznie wysyła (parse_url / new URL())
signature_expiredZegar Twojego serwera się rozjechał. NTP (timedatectl set-ntp true) otwórz; tolerancja ±300 s
signature_replayedWysłano ten sam nonce drugi raz. Przy ponowieniu (retry) zmień nonce i znacznik czasu wygeneruj ponownie i podpisz ponownie
signature_requiredKlucz wymaga podpisu, ale brakuje nagłówków
signature_not_configuredKlucz nie ma sekretu podpisu; wygeneruj „Nowy sekret podpisu” w panelu

Kody błędów

Błędy uwierzytelniania i bezpieczeństwa są takie same we wszystkich punktach końcowych JSON code zwraca wartości. Punkty końcowe sterowania połączeniami i kolejek (zgodne z Verimor) zwracają ten sam kod HTTP z komunikatem w czystym tekście.

HTTPcodeCo robić
401missing_tokenAuthorization: Bearer … dodaj nagłówek
401invalid_tokenKlucz jest błędny, unieważniony lub w ogóle go brak. Nie ponawiaj, popraw ustawienie
401token_expiredOdnów klucz w panelu
401query_key_disabledWysyłaj klucz w nagłówku zamiast w adresie URL
401signature_*Zobacz powyższą tabelę
403ip_not_allowedDodaj adres IP wychodzący swojego serwera do listy klucza
403scope_deniedNadaj kluczowi odpowiednie uprawnienie lub użyj właściwego klucza
403account_inactiveKonto zamknięte; skontaktuj się ze wsparciem
403module_disabledDana usługa nie jest włączona w Twoim pakiecie
413payload_too_largePodziel żądanie na części (np. add_leads maksymalnie 5.000 rekordów)
429rate_limited / tenant_rate_limitedRetry-After poczekaj do
429ip_lockedZ tego adresu IP nadeszło wiele błędnych prób; popraw błędną konfigurację, blokada zniknie samoczynnie

Przykład odpowiedzi z błędem:

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

Limity zapytań

LimitDomyślnie
Na klucz120 żądań na minutę (można obniżyć w ustawieniach klucza)
Łącznie wszystkich kluczy konta600 żądań na minutę
Statusy konsultantów (agent_statuses)dodatkowo 2 żądań na minutę na konto

W każdej udanej odpowiedzi pozostałe limity przychodzą w nagłówkach:

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

429 po otrzymaniu Retry-After Odczekaj (sekundy). Błąd sieci i 5xx użyj wykładniczego wycofywania (1 s, 2 s, 4 s… maksymalnie 5 prób). 401/403 błędy nie ponawiaj: to błąd konfiguracji, próby uruchamiają blokadę brute force.

Odnawianie klucza

Odnawiaj klucze co 90-180 dni, po odejściu pracownika lub w razie podejrzenia wycieku. Płynne przełączenie:

  1. Klucze API > odpowiedni klucz > Ustawienia > Odnów klucz. Wybierz „Stary działa przez 24 godz.”.
  2. Z tymi samymi ustawieniami generowany jest nowy klucz i nowy sekret podpisu, pokazywane jeden raz na ekranie.
  3. Zaktualizuj swoją integrację o nowe wartości.
  4. W dzienniku prefiks starego klucza (bt_7820d84…) nie jest już widoczny, poczekaj; po upływie czasu stary klucz wygaśnie samoczynnie.

W razie podejrzenia włamania Wybierz „Zamknij stary natychmiast”; żądania ze starym kluczem zostaną od razu odrzucone.

Dziennik żądań

pod stroną Klucze API Dziennik żądań pokazuje każde żądanie: czas, prefiks klucza, IP, punkt końcowy i akcję, status HTTP, kod błędu, czas trwania, czy podpisane. Podsumowanie ostatnich 24 godzin (żądania, błędy, limit zapytań, błędy uwierzytelniania, żądania podpisane, liczba różnych IP) znajduje się u góry strony. Rekordy są przechowywane 90 dni.

W każdej odpowiedzi X-Request-Id jest nagłówek. Podaj tę wartość w zgłoszeniu do wsparcia; od razu znajdziemy Twoje żądanie w dzienniku. Nigdy nie umieszczaj klucza ani sekretu podpisu w zgłoszeniu do pomocy, e-mailu ani zrzucie ekranu.

Jeśli zobaczysz żądanie z nieznanego IP lub w nieoczekiwanych godzinach, natychmiast odnów klucz opcją „Zamknij stary natychmiast”.

Konfiguracja Byfix CRM

Byfix CRM łączy się z Buluthat za pomocą dwóch oddzielnych tożsamości:

Ustawienie (CRM)Wartość
Ustawienia > Automatyczne połączenia > Klucz APIWygenerowane w Buluthat bt_… klucz (zakres: autocall + voicebot + voice_otp + call)
Ustawienia > Automatyczne połączenia > Sekret podpisuTen sam klucz bts_… sekret. Jeśli wypełniony, każde żądanie wysyłane przez CRM do Buluthat jest podpisywane
Ustawienia VoIP > Buluthat API TokenEkran na żywo / most CDR (crm_bridge); wydaje zespół Buluthat, jest odrębny od powyższego klucza

Zalecana kolejność: klucz Podpisane żądanie wymagane wygeneruj z wyłączonym, wpisz klucz i sekret w CRM, w dzienniku żądania imzalı zobacz, że przychodzi z odznaką, następnie włącz w kluczu obowiązkowy podpis. Dodaj do klucza także adres IP serwera CRM.

Click-to-call (begin_call) przesyła teraz klucz w nagłówku, a nie w adresie URL. Po aktualizacji CRM możesz wyłączyć w starym kluczu uprawnienie „Klucz w adresie URL”.

Konfiguracja ByCRM

W ByCRM każda firma do Buluthat własnym kluczem jest powiązany; firmy nie widzą nawzajem swoich danych.

  1. W panelu Buluthat z kontem firmy Klucze API > Nowy klucz: uprawnienia Dane centrali na żywo, Sterowanie połączeniami i kolejki, Automatyczne połączenia (jeśli jest używany Asystent Głosowy, Głosowy kod weryfikacyjny). Podaj adres IP serwera ByCRM.
  2. ByCRM > Integracje > Buluthat:
PoleWartość
Buluthat API Tokenbt_…
sekret podpisubts_…
Bridge Tokento samo bt_… klucz
CRM Bridge URLhttps://api.buluthat.com/api/crm_bridge.php
Autocall URLhttps://api.buluthat.com/api/autocall.php
Buluthat Tenant ID / PBX Server IDNie jest używany; zwracane są dane konta, do którego należy klucz
  1. Wypróbuj ekran na żywo i click-to-call, w dzienniku imzalı zobacz odznaki, następnie w kluczu Podpisane żądanie wymaganeOtwórz
Ponieważ ekran na żywo odpytuje most co kilka sekund, żądania mostu mają osobny, szerszy limit (na klucz 1.200 na minutę); do dziennika zapisywane są tylko błędne żądania mostu.

Weryfikacja webhooków

Webhooki wysyłane do Ciebie przez Buluthat są również podpisane (X-Buluthat-Signature: sha256=…). Na serwerze podpis surowa treść nie przetwarzaj żadnego webhooka bez weryfikacji przez; X-Buluthat-Delivery nie przetwarzaj dwa razy tego samego dostarczenia. Szczegóły: Webhooki.

Lista kontrolna bezpieczeństwa

  • [ ] Każda integracja ma własny klucz, tylko z wymaganymi zakresami
  • [ ] Klucze nie w kodzie, lecz w zmiennej środowiskowej lub zarządzaniu sekretami (.env Nie trafia do Git)
  • [ ] Klucz tylko po stronie serwera; nie ma go w JavaScript przeglądarki, aplikacji mobilnej, makrze Excela
  • [ ] Lista dozwolonych IP jest wypełniona
  • [ ] Wymagane podpisane żądanie jest włączone
  • [ ] Klucz w adresie URL (?key=) wyłączone
  • [ ] Jest data wygaśnięcia lub w kalendarzu ustawiono przypomnienie o odnowieniu
  • [ ] Klient weryfikuje certyfikat TLS (CURLOPT_SSL_VERIFYPEER włączone); gdy weryfikacja jest wyłączona, ktoś pośredniczący może odczytać i zmienić żądanie
  • [ ] Czas serwera zsynchronizowany przez NTP
  • [ ] Podpis webhooka jest weryfikowany
  • [ ] Dziennik jest przeglądany raz w miesiącu; klucze, do których miał dostęp odchodzący pracownik, zostały odnowione

Zgłoszenie luki w zabezpieczeniach

Jeśli sądzisz, że znalazłeś lukę w zabezpieczeniach API Buluthat, zgłoś to w Centrum Pomocy w panelu „Powiadomienie bezpieczeństwa” załóż zgłoszenie z tematem. Opisz, jak odtworzyć lukę, a jeśli jest X-Request-Id dodaj wartości. Prosimy nie udostępniać szczegółów, dopóki nie sprawdzimy i nie poprawimy zgłoszenia.