امنیت API

API Buluthat سانترال تلفنی شما را مدیریت می‌کند: تماس برقرار می‌کند، تماس را قطع می‌کند، SMS می‌فرستد، شماره‌های مشتریان را پردازش می‌کند. به همین دلیل نشت کلید یعنی نه «یک گزارش دیده می‌شود» بلکه «از حساب شما تماس گرفته می‌شود». این صفحه توضیح می‌دهد چگونه از کلید خود محافظت کنید و Buluthat از طرف شما چه حفاظت‌هایی اعمال می‌کند.

نشانی پایه: https://api.buluthat.com/api/

لایه‌ها در یک نگاه

هر درخواست به‌ترتیب از این دروازه‌ها می‌گذرد. اگر یکی رد کند درخواست پردازش نمی‌شود و در گزارش ثبت می‌شود.

ترتیبدروازهچه می‌کندخطا
1قفل ضد حمله جستجوی فراگیراگر یک IP در 10 دقیقه 20 بار کلید یا امضای نادرست امتحان کند آن IP به‌طور موقت قفل می‌شود429 ip_locked
2محدودیت بدنهبدنه درخواست بزرگ‌تر از 5 مگابایت خوانده نمی‌شود413 payload_too_large
3کلیدکلید فقط در هدر پذیرفته می‌شود؛ در سیستم فقط خلاصه SHA-256 نگهداری می‌شود401 invalid_token
4مدت و لغوکلید منقضی‌شده یا لغوشده رد می‌شود401 token_expired
5IP مجازاگر برای کلید فهرست IP تعریف شده باشد، فقط درخواست‌های آن آدرس‌ها عبور می‌کند403 ip_not_allowed
6وضعیت حسابکلیدهای حساب بسته‌شده یا غیرفعال کار نمی‌کنند403 account_inactive
7دامنهکلید فقط به APIهای مجاز دسترسی دارد403 scope_denied
8امضااگر در کلید «امضای درخواست اجباری» روشن باشد، امضای HMAC، مهر زمانی و nonce یک‌بارمصرف بررسی می‌شود401 signature_*
9محدودیت نرخمحدودیت دقیقه‌ای برای هر کلید و برای مجموع حساب429 rate_limited

شروع سریع

  1. در پنل حساب و پشتیبانی > کلیدهای API صفحه را باز کنید (فقط مدیر حساب می‌بیند).
  2. کلید جدید: یک نام بدهید، فقط دسترسی‌های لازم را علامت بزنید، IP خروجی سرور خود را وارد کنید، امضای اجباری درخواسترا باز کنید.
  3. دو مقداری که فقط یک‌بار روی صفحه نمایش داده می‌شود را کپی کنید: کلید API (bt_…، در هر درخواست Authorization در هدر ارسال می‌شود) و رمز امضا (bts_…، برای امضای درخواست استفاده می‌شود، هرگز ارسال نمی‌شود).
  4. اتصال را امتحان کنید:
curl "https://api.buluthat.com/api/autocall.php?action=ping" \
  -H "Authorization: Bearer bt_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

اگر امضا اجباری باشد این درخواست 401 signature_required برمی‌گردد؛ زیر امضای درخواست از نمونه‌های بخش استفاده کنید.

کلید و رمز امضا در سیستم به‌صورت قابل بازخوانی ذخیره نمی‌شود. اگر آن را گم کنید قابل بازیابی نیست؛ کلید را از پنل تمدید می‌کنید.

دامنه‌ها

هر کلید با یک یا چند دامنه ساخته می‌شود. درخواست به API خارج از دامنه 403 scope_denied برمی‌گردد.

دامنهAPIهایی که باز می‌کند
autocallتماس خودکار (api/autocall.php). برای سازگاری با یکپارچه‌سازی‌های قدیمی، مسیرهای دستیار صوتی، تأیید صوتی و کنترل تماس را نیز باز می‌کند
callکنترل تماس و صف‌ها: begin_call, bridge, hangup, transfer, mute, queues, queues_pending, queue_user_list, queue_manage_users, agent_statuses
voicebotدستیار صوتی (api/voicebot_api.php) و تأیید صوتی
voice_otpکد تأیید صوتی (api/voice_otp.php)
smsAPI SMS (api/sms.php). هیچ دامنه دیگری به SMS دسترسی ندارد
bridgeداده زنده سانترال (api/crm_bridge.php): تماس‌های زنده، وضعیت نمایندگان، ضبط تماس‌ها، فایل صوتی، لیست سیاه، پیام‌های صوتی. فقط حساب مربوط به کلید؛ ارسال‌شده tenant_id نادیده گرفته می‌شود
*همه APIها. فقط اگر واقعاً لازم باشد

اصل: برای هر یکپارچه‌سازی کلید جدا، برای هر کلید حداقل دسترسی. اگر کلید SMS سایت تجارت الکترونیک شما نشت کند مهاجم نمی‌تواند تماس برقرار کند؛ فقط همان کلید را لغو می‌کنید.

عدم ارسال کلید

کلید در هدر ارسال می‌شود:

Authorization: Bearer bt_xxxxxxxx

Authorization برای محیط‌هایی که نمی‌توانند هدر را تنظیم کنند X-Api-Key: bt_xxxxxxxx نیز پذیرفته می‌شود.

کلید در URL (?key=)

?key=bt_… قالب در کلیدهای جدید بسته است و 401 query_key_disabled برمی‌گردد. URLها در لاگ وب‌سرور، ثبت‌های پراکسی، تاریخچه مرورگر و Referer در هدر می‌افتد؛ کلید از آنجا نشت می‌کند. فقط برای یک سیستم قدیمی که نمی‌تواند هدر بفرستد، در تنظیمات کلید «پذیرش کلید در URL» قابل باز شدن است. پنل این کلیدها را قرمز ?key= با نشان نمایش می‌دهد.

در کلیدهای ساخته‌شده پیش از V54 برای قطع نشدن یکپارچه‌سازی‌های قدیمی این مجوز باز گذاشته شده است. پس از انتقال یکپارچه‌سازی‌تان به هدر آن را ببندید.

IP مجاز

در تنظیمات کلید آدرس‌های IP مجاز در فیلد به ازای هر خط یک IP یا بلوک CIDR نوشته می‌شود:

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

اگر فهرست خالی باشد هر IP پذیرفته می‌شود. اگر پر باشد درخواست از آدرس‌هایی خارج از فهرست حتی با کلید درست 403 ip_not_allowed برمی‌گردد. نشانی‌ای که باید نوشته شود، فراخواننده API IP خروجی سرور شماست (نه رایانه خودتان). اگر مطمئن نیستید، در ستون IP درخواست‌های همان سرور در گزارش را ببینید.

امضای درخواست

امضا باعث می‌شود حتی اگر کلید به دست دیگری بیفتد نتوان درخواست فرستاد: مهاجم به رمز امضا هم نیاز دارد و این رمز در هیچ درخواستی از شبکه عبور نمی‌کند. امضا همچنین:

  • بدنه را قفل می‌کند: اگر یک نویسه در مسیر تغییر کند امضا نمی‌خواند.
  • مانع از پخش مجدد می‌شود: هر nonce فقط یک‌بار پذیرفته می‌شود؛ درخواستی که شنود شده باشد دوباره ارسال نمی‌شود.
  • درخواست کهنه را رد می‌کند: اگر مهر زمانی بیش از ±5 دقیقه از ساعت سرور منحرف شود درخواست رد می‌شود.

در کلید امضای اجباری درخواست اگر باز باشد هر درخواست باید امضاشده باشد. حتی اگر بسته باشد، اگر هدرهای امضا را ارسال کنید امضا باز هم تأیید می‌شود؛ امضای نادرست بی‌صدا عبور نمی‌کند.

سرشناسه‌ها

سرشناسهمقدار
AuthorizationBearer bt_…
X-Bt-Timestampزمان یونیکس، ثانیه (مثلاً 1790802088)
X-Bt-Nonceدر هر درخواست جدید، 16-64 نویسه A-Z a-z 0-9 _ - (مثلاً 32 هگز)
X-Bt-Signaturev1= + حالت هگز با حروف کوچک امضا

متن مرجع

متنی که امضا می‌شود، در میان آن‌ها \n (LF) شش خط است:

v1
POST
/api/sms.php?action=send
1790802088
9f86d081884c7d659a2feaa0c55ad015
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
ردیفمحتوا
1نسخه، ثابت v1
2متد HTTP، با حروف بزرگ (GET, POST)
3مسیر و رشته پرس‌وجو، همان‌طور که درخواست ارسال شده (/api/sms.php?action=send). شامل نام دامنه و طرح‌واره نیست
4X-Bt-Timestamp مقدار
5X-Bt-Nonce مقدار
6خلاصه SHA-256 بدنه، هگز با حروف کوچک. در درخواست بدون بدنه و multipart/form-data خلاصه بدنه خالی در درخواست‌های (بارگذاری فایل): e3b0c442…b855

امضا: 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'],
]));

کلاینت‌های آماده هم امضا می‌کنند: buluthat-autocall-client.php و buluthat-voice-otp-client.php در پارامتر چهارم ['signing_secret' => 'bts_…'] می‌گیرد.

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

خط فرمان (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"

چرا امضا نمی‌خواند؟

نشانهدلیل
signature_invalid فقط در POSTبدنه‌ای که امضا کرده‌اید با بدنه‌ای که فرستاده‌اید متفاوت است. JSON را یک‌بار تولید کنید، همان متغیر را هم به خلاصه هم به درخواست بدهید
signature_invalid در جستجو با نویسه‌های ترکیمسیر را خودتان ترکیب کرده و متفاوت کدگذاری کرده‌اید. در امضا مسیر و پرس‌وجوی همان URL که کلاینت واقعاً ارسال کرده را استفاده کنید (parse_url / new URL())
signature_expiredساعت سرور شما جابه‌جا شده است. NTP (timedatectl set-ntp true) را باز کنید؛ تلورانس ±300 ثانیه
signature_replayedهمان nonce دوباره ارسال شد. در تلاش مجدد (retry) nonce و مهر زمانی را دوباره تولید کنید و دوباره امضا کنید
signature_requiredامضا برای کلید اجباری است اما هدرها ناقص‌اند
signature_not_configuredکلید رمز امضا ندارد؛ از پنل «رمز امضای جدید» بسازید

کدهای خطا

خطاهای هویت و امنیت در همه نقاط JSON یکسان است code با مقادیر برمی‌گردد. نقاط کنترل تماس و صف (سازگار با Verimor) همان کد HTTP را با پیام متن ساده برمی‌گردانند.

HTTPcodeچه باید کرد
401missing_tokenAuthorization: Bearer … هدر را اضافه کنید
401invalid_tokenکلید اشتباه، باطل‌شده یا اصلاً وجود ندارد. دوباره امتحان نکنید، تنظیم را اصلاح کنید
401token_expiredکلید را از پنل تمدید کنید
401query_key_disabledکلید را به‌جای URL در هدر ارسال کنید
401signature_*جدول بالا را ببینید
403ip_not_allowedIP خروجی سرور خود را به فهرست کلید اضافه کنید
403scope_deniedدسترسی مربوطه را به کلید بدهید یا از کلید درست استفاده کنید
403account_inactiveحساب بسته است؛ با پشتیبانی صحبت کنید
403module_disabledخدمت مربوطه در بسته شما فعال نیست
413payload_too_largeدرخواست را به قطعات تقسیم کنید (مثلاً add_leads حداکثر 5.000 رکورد)
429rate_limited / tenant_rate_limitedRetry-After صبر کنید
429ip_lockedاز این IP تعداد زیادی تلاش نادرست آمده است؛ پیکربندی نادرست را اصلاح کنید، قفل خودبه‌خود برداشته می‌شود

نمونه پاسخ خطا:

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

محدودیت‌های نرخ

محدودیتپیش‌فرض
برای هر کلید120 درخواست در دقیقه (از تنظیم کلید قابل کاهش است)
مجموع همه کلیدهای حساب600 درخواست در دقیقه
وضعیت نمایندگان (agent_statuses)علاوه بر آن 2 درخواست در دقیقه برای هر حساب

در هر پاسخ موفق، سهمیه باقی‌مانده در هدرها می‌آید:

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

429 هنگام دریافت Retry-After (ثانیه) صبر کنید. خطای شبکه و 5xx برای آن عقب‌نشینی نمایی استفاده کنید (1 ثانیه، 2 ثانیه، 4 ثانیه… حداکثر 5 تلاش). 401/403 خطاهای دوباره امتحان نکنید: خطای پیکربندی است، تلاش‌ها قفل ضد حمله جستجوی فراگیر را فعال می‌کنند.

تمدید کلید

کلیدها را هر 90-180 روز، هنگام خروج کارمند یا در صورت شک به نشت تمدید کنید. انتقال بدون وقفه:

  1. کلیدهای API > کلید مربوطه > تنظیمات > تمدید کلید. «کلید قدیمی 24 ساعت کار کند» را انتخاب کنید.
  2. با همان تنظیمات کلید جدید و رمز امضای جدید تولید می‌شود، یک‌بار روی صفحه نمایش داده می‌شود.
  3. یکپارچه‌سازی خود را با مقادیر جدید به‌روزرسانی کنید.
  4. در گزارش پیشوند کلید قدیمی (bt_7820d84…) دیگر نمایش داده نمی‌شود، صبر کنید؛ با پایان مهلت کلید قدیمی خودبه‌خود بسته می‌شود.

در صورت شک به نشت «بستن فوری کلید قدیمی» را انتخاب کنید؛ درخواست‌های کلید قدیمی بلافاصله رد می‌شوند.

گزارش درخواست‌ها

در پایین صفحه کلیدهای API گزارش درخواست‌ها هر درخواست را نشان می‌دهد: زمان، پیشوند کلید، IP، نقطه و عملیات، وضعیت HTTP، کد خطا، مدت، امضاشده یا نه. خلاصه 24 ساعت اخیر (درخواست، خطا، محدودیت نرخ، خطای هویت، درخواست امضاشده، تعداد IP متفاوت) بالای صفحه است. ثبت‌ها 90 روز نگهداری می‌شود.

در هر پاسخ X-Request-Id هدر وجود دارد. این مقدار را در درخواست پشتیبانی بنویسید؛ درخواست شما را فوراً در گزارش پیدا می‌کنیم. کلید یا رمز امضا را هرگز در درخواست پشتیبانی، ایمیل یا اسکرین‌شات قرار ندهید.

اگر IP ناشناس یا درخواست در ساعاتی که انتظار ندارید دیدید کلید را فوراً با «بستن فوری کلید قدیمی» تمدید کنید.

راه‌اندازی Byfix CRM

Byfix CRM با دو هویت جداگانه به Buluthat وصل می‌شود:

تنظیم (CRM)مقدار
تنظیمات > تماس خودکار > کلید APIتولیدشده در Buluthat bt_… کلید (دامنه: autocall + voicebot + voice_otp + call)
تنظیمات > تماس خودکار > رمز امضاهمان کلید bts_… رمز. اگر پر باشد هر درخواستی که CRM به Buluthat می‌فرستد امضا می‌شود
تنظیمات VoIP > توکن API Buluthatصفحه زنده / پل CDR (crm_bridge); تیم Buluthat می‌دهد و از کلید بالا جداست

ترتیب پیشنهادی: کلید را امضای اجباری درخواست بسته تولید کنید، کلید و رمز را در CRM وارد کنید، در گزارش درخواست‌های imzalı با نشان ببینید می‌آید، سپس در کلید امضا را اجباری کنید. IP سرور CRM را هم به کلید اضافه کنید.

کلیک برای تماس (begin_call) دیگر کلید را در هدر ارسال می‌کند، نه در URL. پس از به‌روزرسانی CRM می‌توانید مجوز «کلید در URL» را در کلید قدیمی ببندید.

راه‌اندازی ByCRM

در ByCRM هر شرکت به Buluthat با کلید خودش وصل می‌شود؛ شرکت‌ها داده یکدیگر را نمی‌بینند.

  1. در پنل Buluthat با حساب شرکت کلیدهای API > کلید جدید: دسترسی‌ها داده زنده سانترال, کنترل تماس و صف‌ها, تماس خودکار (در صورت استفاده دستیار صوتی, کد تأیید صوتی). سرور ByCRM را وارد کنید. IP
  2. ByCRM > یکپارچه‌سازی‌ها > Buluthat:
حوزهمقدار
توکن API Buluthatbt_…
رمز امضاbts_…
Bridge Tokenهمان bt_… کلید
CRM Bridge URLhttps://api.buluthat.com/api/crm_bridge.php
Autocall URLhttps://api.buluthat.com/api/autocall.php
Buluthat Tenant ID / PBX Server IDاستفاده نمی‌شود؛ داده حساب صاحب کلید می‌آید
  1. صفحه زنده و کلیک برای تماس را امتحان کنید، در گزارش imzalı نشان‌ها را ببینید، سپس در کلید امضای اجباری درخواسترا باز کنید.
چون صفحه زنده هر چند ثانیه پل را فراخوانی می‌کند، درخواست‌های پل محدودیت نرخ جدا و گسترده‌تری دارند (برای هر کلید 1.200 در دقیقه)؛ در گزارش فقط درخواست‌های ناموفق پل ثبت می‌شود.

تأیید Webhookها

Webhookهایی که Buluthat برای شما می‌فرستد هم امضادار هستند (X-Buluthat-Signature: sha256=…). امضا را در سرور خود بدنه خام هیچ Webhook را بدون تأیید از طریق پردازش نکنید؛ X-Buluthat-Delivery همان تحویل را دو بار پردازش نکنید. جزئیات: Webhookها.

فهرست بررسی امنیت

  • [ ] هر یکپارچه‌سازی کلید خودش را دارد، فقط با دامنه‌های لازم
  • [ ] کلیدها در کد نیست بلکه در متغیر محیطی یا مدیریت رازها است (.env وارد Git نمی‌شود)
  • [ ] کلید فقط در سمت سرور است؛ در JavaScript مرورگر، اپلیکیشن موبایل و ماکروی Excel نیست
  • [ ] فهرست IP مجاز پر است
  • [ ] امضای اجباری درخواست روشن است
  • [ ] کلید در URL (?key=) غیرفعال
  • [ ] تاریخ انقضا دارد یا یادآوری تمدید در تقویم تنظیم شده
  • [ ] کلاینت گواهی TLS را تأیید می‌کند (CURLOPT_SSL_VERIFYPEER باز است)؛ وقتی تأیید بسته باشد فردی که در میانه قرار گیرد می‌تواند درخواست را بخواند و تغییر دهد
  • [ ] ساعت سرور با NTP همگام است
  • [ ] امضای Webhook تأیید می‌شود
  • [ ] گزارش ماهی یک‌بار بازبینی می‌شود؛ کلیدهایی که کارمند ترک‌کرده به آن‌ها دسترسی داشت تمدید شده

گزارش آسیب‌پذیری امنیتی

اگر فکر می‌کنید در API Buluthat آسیب‌پذیری امنیتی پیدا کرده‌اید، از مرکز پشتیبانی داخل پنل «اعلان امنیتی» درخواستی با موضوع باز کنید. نحوه بازتولید آسیب‌پذیری و در صورت وجود X-Request-Id مقادیر را اضافه کنید. تا زمانی که اعلان را بررسی و اصلاح کنیم از به اشتراک گذاشتن جزئیات خودداری کنید.