API Təhlükəsizliyi

Buluthat API-si telefon santralınızı idarə edir: zəng başladır, zəngi bağlayır, SMS göndərir, müştəri nömrələrini emal edir. Buna görə açarın sızması "hesabat görünür" yox, "hesabınızdan zəng edilir" deməkdir. Bu səhifə açarınızı necə qoruyacağınızı və Buluthat-ın sizin əvəzinizə hansı qorumaları tətbiq etdiyini izah edir.

Əsas ünvan: https://api.buluthat.com/api/

Qatlar bir baxışda

Hər sorğu ardıcıllıqla bu qapılardan keçir. Biri rədd edərsə sorğu emal olunmur və jurnala yazılır.

SıraQapıNə edirXəta
1Kobud qüvvə kilidiBir IP 10 dəqiqədə 20 dəfə yanlış açar ya da imza sınayarsa həmin IP müvəqqəti kilidlənir429 ip_locked
2Gövdə limiti5 MB-dan böyük sorğu gövdəsi oxunmur413 payload_too_large
3AçarAçar yalnız başlıqda qəbul edilir; sistemdə yalnız SHA-256 xülasəsi saxlanılır401 invalid_token
4Müddət və ləğvMüddəti bitmiş ya da ləğv edilmiş açar rədd edilir401 token_expired
5İcazəli IPAçara IP siyahısı təyin edilibsə yalnız həmin ünvanlardan gələn sorğu keçir403 ip_not_allowed
6Hesabın vəziyyətiBağlanmış ya da passiv hesabın açarları işləmir403 account_inactive
7ƏhatəAçar yalnız icazə verilən API-lərə daxil olur403 scope_denied
8İmzaAçarda "imzalı sorğu məcburidir" açıqdırsa HMAC imzası, vaxt damğası və birdəfəlik nonce doğrulanır401 signature_*
9Sürət limitiAçar başına və hesab cəmində dəqiqəlik limit429 rate_limited

Sürətli başlanğıc

  1. Paneldə Hesab və Dəstək > API Açarları səhifəsini açın (yalnız hesab səlahiyyətlisi görür).
  2. Yeni açar: ad verin, yalnız lazım olan səlahiyyətləri işarələyin, serverinizin çıxış IP-sini yazın, İmzalı sorğu məcburidir-nu açın.
  3. Ekranda bir dəfə göstərilən iki dəyəri kopyalayın: API açarı (bt_…, hər sorğuda Authorization başlığında gedir) və imza sirri (bts_…, sorğunu imzalamaq üçün istifadə olunur, heç vaxt göndərilmir).
  4. Bağlantını yoxlayın:
curl "https://api.buluthat.com/api/autocall.php?action=ping" \
  -H "Authorization: Bearer bt_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

İmza məcburidirsə bu sorğu 401 signature_required qaytarır; aşağıdakı Sorğu imzalama bölməsindəki nümunələrdən birini istifadə edin.

Açar və imza sirri sistemdə geri oxuna bilən formada saxlanılmır. İtirsəniz bərpa edə bilmərik; paneldən açarı yeniləyirsiniz.

Əhatələr

Hər açar bir və ya bir neçə əhatə ilə yaradılır. Əhatədən kənar API-yə gələn sorğu 403 scope_denied qaytarır.

ƏhatəAçdığı API-lər
autocallAvtomatik Zəng (api/autocall.php). Köhnə inteqrasiyalarla uyğunluq üçün səsli asistent, səsli doğrulama və zəng idarəetmə nöqtələrini də açır
callZəng idarəetməsi və növbələr: begin_call, bridge, hangup, transfer, mute, queues, queues_pending, queue_user_list, queue_manage_users, agent_statuses
voicebotSəsli Asistent (api/voicebot_api.php) və səsli doğrulama
voice_otpSəsli Doğrulama Kodu (api/voice_otp.php)
smsSMS API (api/sms.php). Başqa heç bir əhatə SMS-ə çıxış əldə edə bilməz
bridgeCanlı santral məlumatı (api/crm_bridge.php): canlı zənglər, nümayəndə vəziyyəti, zəng qeydləri, səs qeydi, qara siyahı, anonslar. Yalnız açarın hesabı; göndərilən tenant_id nəzərə alınmır
*Bütün API-lər. Yalnız həqiqətən lazımdırsa

Prinsip: hər inteqrasiyaya ayrı açar, hər açara ən az səlahiyyət. E-ticarət saytınızın SMS açarı sızarsa hücumçu zəng başlada bilməz; yalnız həmin açarı ləğv edirsiniz.

Açarı göndərmə

Açar başlıqda göndərilir:

Authorization: Bearer bt_xxxxxxxx

Authorization başlığını ayarlaya bilməyən mühitlər üçün X-Api-Key: bt_xxxxxxxx də qəbul edilir.

URL-də açar (?key=)

?key=bt_… formatı yeni açarlarda bağlıdır və 401 query_key_disabled qaytarır. URL-lər veb server jurnallarına, proksi qeydlərinə, brauzer tarixçəsinə və Referer başlığına düşür; açar oradan sızır. Yalnız başlıq göndərə bilməyən köhnə sistem üçün, açar ayarlarında "Açarı URL-də qəbul et" açıla bilər. Panel bu açarları qırmızı ?key= nişanı ilə göstərir.

V54 əvvəl yaradılmış açarlarda köhnə inteqrasiyalar qopmasın deyə bu icazə açıq saxlanıldı. İnteqrasiyanızı başlığa köçürdükdən sonra bağlayın.

İcazəli IP

Açar ayarlarındakı İcazəli IP ünvanları sahəsinə sətir başına bir IP ya da CIDR bloku yazılır:

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

Siyahı boşdursa hər IP qəbul edilir. Doludursa siyahıdakı ünvanlardan kənardan gələn sorğu açar düzgün olsa belə 403 ip_not_allowed qaytarır. Yazılacaq ünvan, API-ni çağıran serverinizin çıxış IP-sidir (öz kompüterinizin deyil). Əmin deyilsinizsə, jurnalda həmin serverdən gələn sorğunun IP sütununa baxın.

Sorğu imzalama

İmza, açar ələ keçirilsə belə sorğu göndərilə bilməməsini təmin edir: hücumçunun ayrıca imza sirrinə ehtiyacı var və bu sirr heç bir sorğuda şəbəkə üzərindən getmir. İmza eyni zamanda:

  • Gövdəni kilidləyir: yolda tək bir simvol dəyişərsə imza tutmur.
  • Təkrar oxutmanı əngəlləyir: hər nonce yalnız bir dəfə qəbul edilir; tutulan sorğu ikinci dəfə göndərilə bilməz.
  • Köhnəlmiş sorğunu rədd edir: vaxt damğası server saatından ±5 dəqiqədən çox sapsa sorğu rədd edilir.

Açarda İmzalı sorğu məcburidir açıqdırsa hər sorğu imzalı olmalıdır. Bağlı olsa belə imza başlıqlarını göndərsəniz imza yenə doğrulanır; yanlış imza səssizcə keçmir.

Başlıqlar

BaşlıqDəyər
AuthorizationBearer bt_…
X-Bt-TimestampUnix vaxtı, saniyə (məs. 1790802088)
X-Bt-NonceHər sorğuda yeni, 16-64 simvol A-Z a-z 0-9 _ - (məs. 32 hex)
X-Bt-Signaturev1= + imzanın kiçik hərfli hex forması

Kanonik mətn

İmzalanan mətn, aralarında \n (LF) olan altı sətirdir:

v1
POST
/api/sms.php?action=send
1790802088
9f86d081884c7d659a2feaa0c55ad015
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
SətirMəzmun
1Versiya, sabit v1
2HTTP metodu, böyük hərflə (GET, POST)
3Yol və sorğu sətri, sorğunun göndərildiyi halı ilə (/api/sms.php?action=send). Domen adı və sxem daxil deyil
4X-Bt-Timestamp dəyəri
5X-Bt-Nonce dəyəri
6Gövdənin SHA-256 xülasəsi, kiçik hərfli hex. Gövdəsiz sorğuda və multipart/form-data (fayl yükləmə) sorğularında boş mətnin özəti: e3b0c442…b855

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

Hazır klientlər də imzalayır: buluthat-autocall-client.php və buluthat-voice-otp-client.php dördüncü parametrdə ['signing_secret' => 'bts_…'] alır.

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

Komanda sətri (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"

İmza niyə tutmur?

ƏlamətSəbəb
signature_invalid yalnız POST-daİmzaladığınız gövdə ilə göndərdiyiniz gövdə fərqlidir. JSON-u bir dəfə yaradın, eyni dəyişəni həm xülasəyə həm sorğuya verin
signature_invalid Türk hərfli sorğudaYolu özünüz birləşdirib fərqli kodlaşdırdınız. İmzada, klientin həqiqətən göndərdiyi URL-dəki yolu və sorğunu istifadə edin (parse_url / new URL())
signature_expiredServerinizin saatı sürüşüb. NTP (timedatectl set-ntp true) açın; tolerans ±300 san
signature_replayedEyni nonce ikinci dəfə göndərildi. Təkrar cəhddə (retry) nonce və vaxt damğasını yenidən yaradıb yenidən imzalayın
signature_requiredAçarda imza məcburidir, lakin başlıqlar çatışmır
signature_not_configuredAçarın imza sirri yoxdur; paneldən "Yeni imza sirri" yaradın

Xəta kodları

Kimlik və təhlükəsizlik xətaları bütün JSON nöqtələrində eynidir code dəyərləri ilə qayıdır. Zəng idarəetməsi və növbə nöqtələri (Verimor uyğun) eyni HTTP kodunu düz mətn mesajla qaytarır.

HTTPcodeNə etməli
401missing_tokenAuthorization: Bearer … başlığını əlavə edin
401invalid_tokenAçar yanlışdır, ləğv edilib ya da heç yoxdur. Təkrar cəhd etməyin, ayarı düzəldin
401token_expiredPaneldən açarı yeniləyin
401query_key_disabledAçarı URL əvəzinə başlıqda göndərin
401signature_*Yuxarıdakı cədvələ baxın
403ip_not_allowedServerinizin çıxış IP-sini açarın siyahısına əlavə edin
403scope_deniedAçara müvafiq səlahiyyəti verin ya da düzgün açarı istifadə edin
403account_inactiveHesab bağlıdır; dəstəklə əlaqə saxlayın
403module_disabledƏlaqəli xidmət paketinizdə açıq deyil
413payload_too_largeSorğunu hissələrə bölün (məs. add_leads ən çox 5.000 qeyd)
429rate_limited / tenant_rate_limitedRetry-After qədər gözləyin
429ip_lockedBu IP-dən çoxlu sayda yanlış cəhd gəldi; yanlış konfiqurasiyanı düzəldin, kilid öz-özünə açılır

Xəta cavabı nümunəsi:

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

Sürət limitləri

LimitDefolt
Açar başınadəqiqədə 120 sorğu (açar ayarından azaldıla bilər)
Hesabın bütün açarlarının cəmidəqiqədə 600 sorğu
Nümayəndə vəziyyətləri (agent_statuses)əlavə olaraq hesab başına dəqiqədə 2 sorğu

Hər uğurlu cavabda qalan hüquq başlıqlarda gəlir:

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

429 aldıqda Retry-After (saniyə) qədər gözləyin. Şəbəkə xətası və 5xx üçün eksponensial geri çəkilmə istifadə edin (1 san, 2 san, 4 san… ən çox 5 cəhd). 401/403 xətalarını təkrar cəhd etməyin: konfiqurasiya xətasıdır, cəhdlər kobud qüvvə kilidini işə salır.

Açar yeniləmə

Açarları 90-180 gündən bir, işçi ayrılanda ya da sızma şübhəsində yeniləyin. Fasiləsiz keçid:

  1. API Açarları > müvafiq açar > Ayarlar > Açarı yenilə. "Köhnəsi 24 saat işləsin" seçin.
  2. Eyni ayarlarla yeni açar və yeni imza sirri yaradılır, ekranda bir dəfə göstərilir.
  3. İnteqrasiyanızı yeni dəyərlərlə yeniləyin.
  4. Jurnalda köhnə açarın prefiksi (bt_7820d84…) artıq görünmürsə gözləyin; müddət bitəndə köhnə açar öz-özünə bağlanır.

Sızma şübhəsində "Köhnəsini dərhal bağla" seçin; köhnə açarla gələn sorğular dərhal rədd edilir.

Sorğu jurnalı

API Açarları səhifəsinin altındakı Sorğu jurnalı hər sorğunu göstərir: vaxt, açar prefiksi, IP, nöqtə və əməliyyat, HTTP vəziyyəti, xəta kodu, müddət, imzalıdır ya yox. Son 24 saatın xülasəsi (sorğu, xəta, sürət limiti, kimlik xətası, imzalı sorğu, fərqli IP sayı) səhifənin üstündədir. Qeydlər 90 gün saxlanılır.

Hər cavabda X-Request-Id başlığı var. Dəstək sorğusunda bu dəyəri yazın; sorğunuzu jurnalda dərhal tapırıq. Açarı ya da imza sirrini dəstək sorğusuna, e-poçta ya da ekran görüntüsünə heç vaxt qoymayın.

Tanımadığınız bir IP ya da gözləmədiyiniz saatlarda sorğu görsəniz açarı dərhal "Köhnəsini dərhal bağla" ilə yeniləyin.

Byfix CRM quraşdırması

Byfix CRM Buluthat-a iki ayrı kimliklə qoşulur:

Ayar (CRM)Dəyər
Ayarlar > Avtomatik Zəng > API AçarıBuluthat-da yaradılan bt_… açarı (əhatə: autocall + voicebot + voice_otp + call)
Ayarlar > Avtomatik Zəng > İmza SirriEyni açarın bts_… sirri. Doludursa CRM-in Buluthat-a göndərdiyi hər sorğu imzalanır
VoIP ayarları > Buluthat API TokenCanlı ekran / CDR körpüsü (crm_bridge); Buluthat komandası verir, yuxarıdakı açardan fərqlidir

Tövsiyə olunan sıra: açarı İmzalı sorğu məcburidir bağlı yaradın, CRM-ə açarı və sirri daxil edin, jurnalda sorğuların imzalı nişanı ilə gəldiyini görün, sonra açarda imzanı məcburi edin. Açara CRM serverinin IP-sini də əlavə edin.

Kliklə-zəng (begin_call) artıq açarı URL-də deyil, başlıqda göndərir. CRM yeniləməsindən sonra köhnə açardakı "URL-də açar" icazəsini bağlaya bilərsiniz.

ByCRM quraşdırması

ByCRM-də hər firma Buluthat-a öz açarı ilə bağlanır; firmalar bir-birinin məlumatını görə bilməz.

  1. Buluthat panelində firmanın hesabı ilə API Açarları > Yeni açar: səlahiyyətlər Canlı Santral Məlumatı, Zəng İdarəetməsi və Növbələr, Avtomatik Zəng (istifadə olunursa Səsli Asistent, Səsli Doğrulama Kodu). ByCRM serverinin IP-sini yazın.
  2. ByCRM > İnteqrasiyalar > Buluthat:
SahəDəyər
Buluthat API Tokenbt_…
İmza sirribts_…
Bridge Tokeneyni bt_… açarı
CRM Bridge URLhttps://api.buluthat.com/api/crm_bridge.php
Autocall URLhttps://api.buluthat.com/api/autocall.php
Buluthat Tenant ID / PBX Server IDİstifadə olunmur; açar hansı hesaba aiddirsə həmin hesabın məlumatı gəlir
  1. Canlı ekranı və kliklə-zəngi sınayın, jurnalda imzalı nişanlarını görün, sonra açarda İmzalı sorğu məcburidir-nu açın.
Canlı ekran körpünü bir neçə saniyədən bir yoxladığı üçün körpü sorğularının ayrı və daha geniş sürət limiti var (açar başına dəqiqədə 1.200); jurnala yalnız yanlış körpü sorğuları yazılır.

Webhook-ların doğrulanması

Buluthat-ın sizə göndərdiyi webhook-lar da imzalıdır (X-Buluthat-Signature: sha256=…). Serverinizdə imzanı xam gövdə üzərindən doğrulamadan heç bir webhook-u emal etməyin; X-Buluthat-Delivery ilə eyni çatdırılmanı iki dəfə emal etməyin. Ətraflı: Webhook-lar.

Təhlükəsizlik yoxlama siyahısı

  • [ ] Hər inteqrasiyanın öz açarı var, yalnız lazım olan əhatələrlə
  • [ ] Açarlar kodda deyil, mühit dəyişənində ya da gizli idarəetmədədir (.env Git-ə girmir)
  • [ ] Açar yalnız server tərəfindədir; brauzer JavaScript-ində, mobil tətbiqdə, Excel makrosunda yoxdur
  • [ ] İcazəli IP siyahısı doludur
  • [ ] İmzalı sorğu məcburidir açıqdır
  • [ ] URL-də açar (?key=) bağlıdır
  • [ ] Son istifadə tarixi var ya da təqvimdə yeniləmə xatırlatması qurulub
  • [ ] Klient TLS sertifikatını doğrulayır (CURLOPT_SSL_VERIFYPEER açıq); doğrulama bağlı olanda araya girən biri sorğunu oxuyub dəyişə bilər
  • [ ] Server saatı NTP ilə sinxrondur
  • [ ] Webhook imzası doğrulanır
  • [ ] Jurnal ayda bir nəzərdən keçirilir; ayrılan işçinin çıxış əldə etdiyi açarlar yeniləndi

Təhlükəsizlik boşluğu bildirişi

Buluthat API-sində təhlükəsizlik boşluğu tapdığınızı düşünürsünüzsə panel daxilindəki Dəstək Mərkəzindən "Təhlükəsizlik bildirişi" mövzulu qeyd açın. Boşluğu necə təkrar yaratdığınızı və varsa X-Request-Id dəyərlərini əlavə edin. Bildirişi nəzərdən keçirib düzəldənə qədər təfərrüatları paylaşmamağınızı xahiş edirik.