امنیت 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 |
| 5 | IP مجاز | اگر برای کلید فهرست IP تعریف شده باشد، فقط درخواستهای آن آدرسها عبور میکند | 403 ip_not_allowed |
| 6 | وضعیت حساب | کلیدهای حساب بستهشده یا غیرفعال کار نمیکنند | 403 account_inactive |
| 7 | دامنه | کلید فقط به APIهای مجاز دسترسی دارد | 403 scope_denied |
| 8 | امضا | اگر در کلید «امضای درخواست اجباری» روشن باشد، امضای HMAC، مهر زمانی و nonce یکبارمصرف بررسی میشود | 401 signature_* |
| 9 | محدودیت نرخ | محدودیت دقیقهای برای هر کلید و برای مجموع حساب | 429 rate_limited |
شروع سریع
- در پنل حساب و پشتیبانی > کلیدهای API صفحه را باز کنید (فقط مدیر حساب میبیند).
- کلید جدید: یک نام بدهید، فقط دسترسیهای لازم را علامت بزنید، IP خروجی سرور خود را وارد کنید، امضای اجباری درخواسترا باز کنید.
- دو مقداری که فقط یکبار روی صفحه نمایش داده میشود را کپی کنید: کلید API (
bt_…، در هر درخواستAuthorizationدر هدر ارسال میشود) و رمز امضا (bts_…، برای امضای درخواست استفاده میشود، هرگز ارسال نمیشود). - اتصال را امتحان کنید:
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) |
sms | API 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 دقیقه از ساعت سرور منحرف شود درخواست رد میشود.
در کلید امضای اجباری درخواست اگر باز باشد هر درخواست باید امضاشده باشد. حتی اگر بسته باشد، اگر هدرهای امضا را ارسال کنید امضا باز هم تأیید میشود؛ امضای نادرست بیصدا عبور نمیکند.
سرشناسهها
| سرشناسه | مقدار |
|---|---|
Authorization | Bearer bt_… |
X-Bt-Timestamp | زمان یونیکس، ثانیه (مثلاً 1790802088) |
X-Bt-Nonce | در هر درخواست جدید، 16-64 نویسه A-Z a-z 0-9 _ - (مثلاً 32 هگز) |
X-Bt-Signature | v1= + حالت هگز با حروف کوچک امضا |
متن مرجع
متنی که امضا میشود، در میان آنها \n (LF) شش خط است:
v1
POST
/api/sms.php?action=send
1790802088
9f86d081884c7d659a2feaa0c55ad015
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
| ردیف | محتوا |
|---|---|
| 1 | نسخه، ثابت v1 |
| 2 | متد HTTP، با حروف بزرگ (GET, POST) |
| 3 | مسیر و رشته پرسوجو، همانطور که درخواست ارسال شده (/api/sms.php?action=send). شامل نام دامنه و طرحواره نیست |
| 4 | X-Bt-Timestamp مقدار |
| 5 | X-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 را با پیام متن ساده برمیگردانند.
| HTTP | code | چه باید کرد |
|---|---|---|
| 401 | missing_token | Authorization: Bearer … هدر را اضافه کنید |
| 401 | invalid_token | کلید اشتباه، باطلشده یا اصلاً وجود ندارد. دوباره امتحان نکنید، تنظیم را اصلاح کنید |
| 401 | token_expired | کلید را از پنل تمدید کنید |
| 401 | query_key_disabled | کلید را بهجای URL در هدر ارسال کنید |
| 401 | signature_* | جدول بالا را ببینید |
| 403 | ip_not_allowed | IP خروجی سرور خود را به فهرست کلید اضافه کنید |
| 403 | scope_denied | دسترسی مربوطه را به کلید بدهید یا از کلید درست استفاده کنید |
| 403 | account_inactive | حساب بسته است؛ با پشتیبانی صحبت کنید |
| 403 | module_disabled | خدمت مربوطه در بسته شما فعال نیست |
| 413 | payload_too_large | درخواست را به قطعات تقسیم کنید (مثلاً add_leads حداکثر 5.000 رکورد) |
| 429 | rate_limited / tenant_rate_limited | Retry-After صبر کنید |
| 429 | ip_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 روز، هنگام خروج کارمند یا در صورت شک به نشت تمدید کنید. انتقال بدون وقفه:
- کلیدهای API > کلید مربوطه > تنظیمات > تمدید کلید. «کلید قدیمی 24 ساعت کار کند» را انتخاب کنید.
- با همان تنظیمات کلید جدید و رمز امضای جدید تولید میشود، یکبار روی صفحه نمایش داده میشود.
- یکپارچهسازی خود را با مقادیر جدید بهروزرسانی کنید.
- در گزارش پیشوند کلید قدیمی (
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 با کلید خودش وصل میشود؛ شرکتها داده یکدیگر را نمیبینند.
- در پنل Buluthat با حساب شرکت کلیدهای API > کلید جدید: دسترسیها داده زنده سانترال, کنترل تماس و صفها, تماس خودکار (در صورت استفاده دستیار صوتی, کد تأیید صوتی). سرور ByCRM را وارد کنید. IP
- ByCRM > یکپارچهسازیها > Buluthat:
| حوزه | مقدار |
|---|---|
| توکن API Buluthat | bt_… |
| رمز امضا | bts_… |
| Bridge Token | همان bt_… کلید |
| CRM Bridge URL | https://api.buluthat.com/api/crm_bridge.php |
| Autocall URL | https://api.buluthat.com/api/autocall.php |
| Buluthat Tenant ID / PBX Server ID | استفاده نمیشود؛ داده حساب صاحب کلید میآید |
- صفحه زنده و کلیک برای تماس را امتحان کنید، در گزارش
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 مقادیر را اضافه کنید. تا زمانی که اعلان را بررسی و اصلاح کنیم از به اشتراک گذاشتن جزئیات خودداری کنید.
