API Güvenliği

Buluthat API'si telefon santralinizi yönetir: arama başlatır, çağrı kapatır, SMS gönderir, müşteri numaralarını işler. Bu yüzden anahtarın sızması "bir rapor görünür" değil, "hesabınızdan arama yapılır" demektir. Bu sayfa anahtarınızı nasıl koruyacağınızı ve Buluthat'ın sizin yerinize hangi korumaları uyguladığını anlatır.

Temel adres: https://api.buluthat.com/api/

Katmanlar bir bakışta

Her istek sırayla şu kapılardan geçer. Biri reddederse istek işlenmez ve günlüğe yazılır.

SıraKapıNe yaparHata
1Kaba kuvvet kilidiBir IP 10 dakikada 20 kez hatalı anahtar ya da imza denerse o IP geçici olarak kilitlenir429 ip_locked
2Gövde sınırı5 MB'tan büyük istek gövdesi okunmaz413 payload_too_large
3AnahtarAnahtar yalnızca başlıkta kabul edilir; sistemde yalnızca SHA-256 özeti tutulur401 invalid_token
4Süre ve iptalSüresi dolmuş ya da iptal edilmiş anahtar reddedilir401 token_expired
5İzinli IPAnahtara IP listesi tanımlıysa yalnızca o adreslerden gelen istek geçer403 ip_not_allowed
6Hesap durumuKapatılmış ya da pasif hesabın anahtarları çalışmaz403 account_inactive
7KapsamAnahtar yalnızca izin verilen API'lere girer403 scope_denied
8İmzaAnahtarda "imzalı istek zorunlu" açıksa HMAC imzası, zaman damgası ve tek kullanımlık nonce doğrulanır401 signature_*
9Hız sınırıAnahtar başına ve hesap toplamında dakikalık sınır429 rate_limited

Hızlı başlangıç

  1. Panelde Hesap ve Destek > API Anahtarları sayfasını açın (yalnızca hesap yetkilisi görür).
  2. Yeni anahtar: ad verin, yalnızca gereken yetkileri işaretleyin, sunucunuzun çıkış IP'sini yazın, İmzalı istek zorunlu'yu açın.
  3. Ekranda bir kez gösterilen iki değeri kopyalayın: API anahtarı (bt_…, her istekte Authorization başlığında gider) ve imza sırrı (bts_…, isteği imzalamak için kullanılır, hiçbir zaman gönderilmez).
  4. Bağlantıyı deneyin:
curl "https://api.buluthat.com/api/autocall.php?action=ping" \
  -H "Authorization: Bearer bt_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

İmza zorunluysa bu istek 401 signature_required döner; aşağıdaki İstek imzalama bölümündeki örneklerden birini kullanın.

Anahtar ve imza sırrı sistemde geri okunabilir biçimde saklanmaz. Kaybederseniz kurtaramayız; panelden anahtarı yenilersiniz.

Kapsamlar

Her anahtar bir ya da birkaç kapsamla üretilir. Kapsam dışı bir API'ye gelen istek 403 scope_denied döner.

KapsamAçtığı API'ler
autocallOtomatik Arama (api/autocall.php). Eski entegrasyonlarla uyum için sesli asistan, sesli doğrulama ve çağrı kontrolü uçlarını da açar
callÇağrı kontrolü ve kuyruklar: begin_call, bridge, hangup, transfer, mute, queues, queues_pending, queue_user_list, queue_manage_users, agent_statuses
voicebotSesli Asistan (api/voicebot_api.php) ve sesli doğrulama
voice_otpSesli Doğrulama Kodu (api/voice_otp.php)
smsSMS API (api/sms.php). Başka hiçbir kapsam SMS'e erişemez
bridgeCanlı santral verisi (api/crm_bridge.php): canlı çağrılar, temsilci durumu, çağrı kayıtları, ses kaydı, kara liste, anonslar. Yalnızca anahtarın hesabı; gönderilen tenant_id yok sayılır
*Tüm API'ler. Yalnızca gerçekten gerekiyorsa

İlke: her entegrasyona ayrı anahtar, her anahtara en az yetki. E-ticaret sitenizin SMS anahtarı sızarsa saldırgan arama başlatamaz; yalnızca o anahtarı iptal edersiniz.

Anahtarı gönderme

Anahtar başlıkta gönderilir:

Authorization: Bearer bt_xxxxxxxx

Authorization başlığını ayarlayamayan ortamlar için X-Api-Key: bt_xxxxxxxx de kabul edilir.

URL'de anahtar (?key=)

?key=bt_… biçimi yeni anahtarlarda kapalıdır ve 401 query_key_disabled döner. URL'ler web sunucusu loglarına, proxy kayıtlarına, tarayıcı geçmişine ve Referer başlığına düşer; anahtar oradan sızar. Yalnızca başlık gönderemeyen eski bir sistem için, anahtar ayarlarında "Anahtarı URL'de kabul et" açılabilir. Panel bu anahtarları kırmızı ?key= rozetiyle gösterir.

V54 öncesinde üretilmiş anahtarlarda eski entegrasyonlar kopmasın diye bu izin açık bırakıldı. Entegrasyonunuzu başlığa taşıdıktan sonra kapatın.

İzinli IP

Anahtar ayarlarındaki İzinli IP adresleri alanına satır başına bir IP ya da CIDR bloğu yazılır:

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

Liste boşsa her IP kabul edilir. Doluysa listedeki adresler dışından gelen istek anahtar doğru olsa bile 403 ip_not_allowed döner. Yazılacak adres, API'yi çağıran sunucunuzun çıkış IP'sidir (kendi bilgisayarınızın değil). Emin değilseniz günlükte o sunucudan gelen isteğin IP sütununa bakın.

İstek imzalama

İmza, anahtar ele geçirilse bile istek atılamamasını sağlar: saldırganın ayrıca imza sırrına ihtiyacı vardır ve bu sır hiçbir istekte ağ üzerinden gitmez. İmza aynı zamanda:

  • Gövdeyi kilitler: yolda tek bir karakter değişirse imza tutmaz.
  • Tekrar oynatmayı engeller: her nonce yalnızca bir kez kabul edilir; yakalanan bir istek ikinci kez gönderilemez.
  • Eskimiş isteği reddeder: zaman damgası sunucu saatinden ±5 dakikadan fazla saparsa istek reddedilir.

Anahtarda İmzalı istek zorunlu açıksa her istek imzalı olmalıdır. Kapalı olsa bile imza başlıkları gönderirseniz imza yine doğrulanır; yanlış imza sessizce geçmez.

Başlıklar

BaşlıkDeğer
AuthorizationBearer bt_…
X-Bt-TimestampUnix zamanı, saniye (ör. 1790802088)
X-Bt-NonceHer istekte yeni, 16-64 karakter A-Z a-z 0-9 _ - (ör. 32 hex)
X-Bt-Signaturev1= + imzanın küçük harf hex hâli

Kanonik metin

İmzalanan metin, aralarında \n (LF) olan altı satırdır:

v1
POST
/api/sms.php?action=send
1790802088
9f86d081884c7d659a2feaa0c55ad015
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
Satırİçerik
1Sürüm, sabit v1
2HTTP yöntemi, büyük harf (GET, POST)
3Yol ve sorgu dizesi, isteğin gönderildiği hâliyle (/api/sms.php?action=send). Alan adı ve şema dahil değil
4X-Bt-Timestamp değeri
5X-Bt-Nonce değeri
6Gövdenin SHA-256 özeti, küçük harf hex. Gövdesiz istekte ve multipart/form-data (dosya yükleme) isteklerinde boş metnin özeti: 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 istemciler de imzalar: buluthat-autocall-client.php ve buluthat-voice-otp-client.php dördüncü parametrede ['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"}]}))

Komut satırı (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 neden tutmuyor?

BelirtiSebep
signature_invalid yalnızca POST'taİmzaladığınız gövde ile gönderdiğiniz gövde farklı. JSON'u bir kez üretin, aynı değişkeni hem özete hem isteğe verin
signature_invalid Türkçe karakterli sorgudaYolu kendiniz birleştirip farklı kodladınız. İmzada, istemcinin gerçekten gönderdiği URL'deki yolu ve sorguyu kullanın (parse_url / new URL())
signature_expiredSunucunuzun saati kaymış. NTP (timedatectl set-ntp true) açın; tolerans ±300 sn
signature_replayedAynı nonce ikinci kez gitti. Yeniden denemede (retry) nonce ve zaman damgasını yeniden üretip yeniden imzalayın
signature_requiredAnahtarda imza zorunlu ama başlıklar eksik
signature_not_configuredAnahtarın imza sırrı yok; panelden "Yeni imza sırrı" üretin

Hata kodları

Kimlik ve güvenlik hataları tüm JSON uçlarında aynı code değerleriyle döner. Çağrı kontrolü ve kuyruk uçları (Verimor uyumlu) aynı HTTP kodunu düz metin mesajla döner.

HTTPcodeNe yapmalı
401missing_tokenAuthorization: Bearer … başlığını ekleyin
401invalid_tokenAnahtar yanlış, iptal edilmiş ya da hiç yok. Tekrar denemeyin, ayarı düzeltin
401token_expiredPanelden anahtarı yenileyin
401query_key_disabledAnahtarı URL yerine başlıkta gönderin
401signature_*Yukarıdaki tabloya bakın
403ip_not_allowedSunucunuzun çıkış IP'sini anahtarın listesine ekleyin
403scope_deniedAnahtara ilgili yetkiyi verin ya da doğru anahtarı kullanın
403account_inactiveHesap kapalı; destekle görüşün
403module_disabledİlgili hizmet paketinizde açık değil
413payload_too_largeİsteği parçalara bölün (ör. add_leads en fazla 5.000 kayıt)
429rate_limited / tenant_rate_limitedRetry-After kadar bekleyin
429ip_lockedBu IP'den çok sayıda hatalı deneme geldi; hatalı yapılandırmayı düzeltin, kilit kendiliğinden kalkar

Hata cevabı örneği:

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

Hız sınırları

SınırVarsayılan
Anahtar başınadakikada 120 istek (anahtar ayarından düşürülebilir)
Hesabın tüm anahtarları toplamıdakikada 600 istek
Temsilci durumları (agent_statuses)ayrıca hesap başına dakikada 2 istek

Her başarılı cevapta kalan hak başlıklarda gelir:

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

429 alınca Retry-After (saniye) kadar bekleyin. Ağ hatası ve 5xx için üstel geri çekilme kullanın (1 sn, 2 sn, 4 sn… en fazla 5 deneme). 401/403 hatalarını tekrar denemeyin: yapılandırma hatasıdır, denemeler kaba kuvvet kilidini tetikler.

Anahtar yenileme

Anahtarları 90-180 günde bir, personel ayrıldığında ya da sızma şüphesinde yenileyin. Kesintisiz geçiş:

  1. API Anahtarları > ilgili anahtar > Ayarlar > Anahtarı yenile. "Eskisi 24 saat çalışsın" seçin.
  2. Aynı ayarlarla yeni anahtar ve yeni imza sırrı üretilir, ekranda bir kez gösterilir.
  3. Entegrasyonunuzu yeni değerlerle güncelleyin.
  4. Günlükte eski anahtarın öneki (bt_7820d84…) artık görünmüyorsa bekleyin; süre dolunca eski anahtar kendiliğinden kapanır.

Sızma şüphesinde "Eskisini hemen kapat" seçin; eski anahtarla gelen istekler anında reddedilir.

İstek günlüğü

API Anahtarları sayfasının altındaki İstek günlüğü her isteği gösterir: zaman, anahtar öneki, IP, uç ve eylem, HTTP durumu, hata kodu, süre, imzalı mı. Son 24 saatin özeti (istek, hata, hız sınırı, kimlik hatası, imzalı istek, farklı IP sayısı) sayfanın üstündedir. Kayıtlar 90 gün saklanır.

Her cevapta X-Request-Id başlığı vardır. Destek talebinde bu değeri yazın; isteğinizi günlükte hemen buluruz. Anahtarı ya da imza sırrını destek talebine, e-postaya ya da ekran görüntüsüne asla koymayın.

Tanımadığınız bir IP ya da beklemediğiniz saatlerde istek görürseniz anahtarı hemen "Eskisini hemen kapat" ile yenileyin.

Byfix CRM kurulumu

Byfix CRM Buluthat'a iki ayrı kimlikle bağlanır:

Ayar (CRM)Değer
Ayarlar > Otomatik Arama > API AnahtarıBuluthat'ta üretilen bt_… anahtarı (kapsam: autocall + voicebot + voice_otp + call)
Ayarlar > Otomatik Arama > İmza SırrıAynı anahtarın bts_… sırrı. Doluysa CRM'in Buluthat'a attığı her istek imzalanır
VoIP ayarları > Buluthat API TokenCanlı ekran / CDR köprüsü (crm_bridge); Buluthat ekibi verir, yukarıdaki anahtardan ayrıdır

Önerilen sıra: anahtarı İmzalı istek zorunlu kapalı üretin, CRM'e anahtarı ve sırrı girin, günlükte isteklerin imzalı rozetiyle geldiğini görün, sonra anahtarda imzayı zorunlu yapın. Anahtara CRM sunucusunun IP'sini de ekleyin.

Tıkla-ara (begin_call) artık anahtarı URL'de değil başlıkta gönderir. CRM güncellemesinden sonra eski anahtardaki "URL'de anahtar" iznini kapatabilirsiniz.

ByCRM kurulumu

ByCRM'de her firma Buluthat'a kendi anahtarıyla bağlanır; firmalar birbirinin verisini göremez.

  1. Buluthat panelinde firmanın hesabıyla API Anahtarları > Yeni anahtar: yetkiler Canlı Santral Verisi, Çağrı Kontrolü ve Kuyruklar, Otomatik Arama (kullanılıyorsa Sesli Asistan, Sesli Doğrulama Kodu). ByCRM sunucusunun IP'sini yazın.
  2. ByCRM > Entegrasyonlar > Buluthat:
AlanDeğer
Buluthat API Tokenbt_…
İmza sırrıbts_…
Bridge Tokenaynı bt_… anahtarı
CRM Bridge URLhttps://api.buluthat.com/api/crm_bridge.php
Autocall URLhttps://api.buluthat.com/api/autocall.php
Buluthat Tenant ID / PBX Server IDKullanılmaz; anahtar hangi hesaba aitse o hesabın verisi gelir
  1. Canlı ekranı ve tıkla-arayı deneyin, günlükte imzalı rozetlerini görün, sonra anahtarda İmzalı istek zorunlu'yu açın.
Canlı ekran köprüyü birkaç saniyede bir yokladığı için köprü isteklerinin ayrı ve daha geniş bir hız sınırı vardır (anahtar başına dakikada 1.200); günlüğe yalnızca hatalı köprü istekleri yazılır.

Webhook'ların doğrulanması

Buluthat'ın size gönderdiği webhook'lar da imzalıdır (X-Buluthat-Signature: sha256=…). Sunucunuzda imzayı ham gövde üzerinden doğrulamadan hiçbir webhook'u işlemeyin; X-Buluthat-Delivery ile aynı teslimatı iki kez işlemeyin. Ayrıntı: Webhook'lar.

Güvenlik kontrol listesi

  • [ ] Her entegrasyonun kendi anahtarı var, yalnızca gereken kapsamlarla
  • [ ] Anahtarlar kodda değil ortam değişkeninde ya da gizli yönetimde (.env Git'e girmez)
  • [ ] Anahtar yalnızca sunucu tarafında; tarayıcı JavaScript'inde, mobil uygulamada, Excel makrosunda yok
  • [ ] İzinli IP listesi dolu
  • [ ] İmzalı istek zorunlu açık
  • [ ] URL'de anahtar (?key=) kapalı
  • [ ] Son kullanma tarihi var ya da takvimde yenileme hatırlatması kurulu
  • [ ] İstemci TLS sertifikasını doğruluyor (CURLOPT_SSL_VERIFYPEER açık); doğrulama kapalıyken araya giren biri isteği okuyup değiştirebilir
  • [ ] Sunucu saati NTP ile eşitli
  • [ ] Webhook imzası doğrulanıyor
  • [ ] Günlük ayda bir gözden geçiriliyor; ayrılan personelin eriştiği anahtarlar yenilendi

Güvenlik açığı bildirimi

Buluthat API'sinde bir güvenlik açığı bulduğunuzu düşünüyorsanız panel içindeki Destek Merkezi'nden "Güvenlik bildirimi" konulu kayıt açın. Açığı nasıl yeniden ürettiğinizi ve varsa X-Request-Id değerlerini ekleyin. Bildirimi inceleyip düzeltene kadar ayrıntıları paylaşmamanızı rica ederiz.