أمان API
تتحكم Buluthat API في سنترالكم الهاتفي: تبدأ المكالمات وتنهيها وترسل SMS وتعالج أرقام العملاء. لذلك فتسرّب المفتاح لا يعني "ظهور تقرير" بل "إجراء مكالمات من حسابكم". تشرح هذه الصفحة كيف تحمون مفتاحكم وأي حمايات تطبقها Buluthat نيابة عنكم.
العنوان الأساسي: https://api.buluthat.com/api/
الطبقات في نظرة
يمر كل طلب بالبوابات التالية بالترتيب. وإن رفضه أحدها لا يُعالَج الطلب ويُكتب في السجل.
| الدور | البوابة | ماذا يفعل | خطأ |
|---|---|---|---|
| 1 | قفل القوة الغاشمة | إذا جرّب عنوان IP مفتاحًا أو توقيعًا خاطئًا 20 مرات خلال 10 دقائق فيُقفل ذلك العنوان مؤقتًا | 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 | SMS API (api/sms.php). لا يمكن لأي نطاق آخر الوصول إلى SMS |
bridge | بيانات السنترال المباشرة (api/crm_bridge.php): المكالمات المباشرة، حالة الموظفين، تسجيلات المكالمات، التسجيل الصوتي، القائمة السوداء، المقاطع الصوتية. حساب المفتاح فقط؛ المرسَل tenant_id يُتجاهل |
* | جميع واجهات API. فقط إن كان ذلك لازمًا فعلًا |
المبدأ: مفتاح مستقل لكل تكامل، وأقل صلاحية لكل مفتاح. فإذا تسرّب مفتاح SMS لموقعكم الإلكتروني فلا يستطيع المهاجم بدء مكالمات؛ وتلغون ذلك المفتاح وحده.
عدم إرسال المفتاح
المفتاح في الترويسة يُرسَل:
Authorization: Bearer bt_xxxxxxxx
Authorization للبيئات التي لا تستطيع ضبط الترويسة X-Api-Key: bt_xxxxxxxx تُقبل أيضًا.
المفتاح في الرابط (?key=)
?key=bt_… الصيغة في المفاتيح الجديدة مغلق و 401 query_key_disabled يعود. تصل عناوين URL إلى سجلات خادم الويب وسجلات الوكيل (proxy) وسجل المتصفح و Referer تقع في الترويسة؛ ويتسرب المفتاح منها. فقط لنظام قديم لا يستطيع إرسال ترويسة، في إعدادات المفتاح "قبول المفتاح في الرابط" يمكن فتحها. تعرض اللوحة هذه المفاتيح بالأحمر ?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 | وقت Unix بالثواني (مثال: 1790802088) |
X-Bt-Nonce | جديد في كل طلب، 16-64 حرفًا A-Z a-z 0-9 _ - (مثال: 32 hex) |
X-Bt-Signature | v1= + التوقيع بصيغة hex بأحرف صغيرة |
النص المرجعي
النص الموقَّع، بينها \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 للمحتوى، بصيغة hex بأحرف صغيرة. في الطلب بلا محتوى و 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 | أرسل المفتاح في الترويسة بدل الرابط |
| 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 > Buluthat API Token | جسر الشاشة المباشرة / CDR (crm_bridge); يقدمه فريق Buluthat وهو منفصل عن المفتاح أعلاه |
الترتيب الموصى به: المفتاح الطلب الموقّع إلزامي أنشئوه مغلقًا، وأدخلوا المفتاح والسر في CRM، وفي السجل الطلبات imzalı شاهدوا أنها تصل بشارة، ثم اجعلوا التوقيع إلزاميًا في المفتاح. وأضيفوا إلى المفتاح عنوان IP لخادم CRM أيضًا.
النقر للاتصال (begin_call) لم يعد يرسل المفتاح في الرابط بل في الترويسة. بعد تحديث CRM يمكنكم إغلاق إذن "المفتاح في الرابط" في المفتاح القديم.
إعداد ByCRM
في ByCRM كل شركة إلى Buluthat بمفتاحه الخاص يرتبط؛ ولا ترى الشركات بيانات بعضها البعض.
- في لوحة Buluthat بحساب الشركة مفاتيح API > مفتاح جديد: الصلاحيات بيانات السنترال المباشرة, التحكم بالمكالمات والطوابير, الاتصال الآلي (إن كان مستخدمًا المساعد الصوتي, رمز التحقق الصوتي). اكتب عنوان IP لخادم ByCRM.
- ByCRM > التكاملات > Buluthat:
| المنطقة | القيمة |
|---|---|
| Buluthat API Token | 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 المسموحة ممتلئة
- [ ] "الطلب الموقّع إلزامي" مفعّل
- [ ] المفتاح في الرابط (
?key=) مغلق - [ ] له تاريخ انتهاء أو ضُبط تذكير بالتجديد في التقويم
- [ ] يتحقق العميل من شهادة TLS (
CURLOPT_SSL_VERIFYPEERمفتوح)؛ وإذا كان التحقق مغلقًا يمكن لمن يعترض الاتصال أن يقرأ الطلب ويغيّره - [ ] ساعة الخادم مضبوطة بـ NTP
- [ ] يُتحقق من توقيع Webhook
- [ ] يُراجَع السجل شهريًا؛ وجُدّدت المفاتيح التي وصل إليها الموظفون المغادرون
الإبلاغ عن ثغرة أمنية
إن ظننتم أنكم عثرتم على ثغرة أمنية في Buluthat API فمن مركز الدعم داخل اللوحة "إشعار أمني" افتحوا سجلًا بموضوع. واكتبوا كيف أعدتم إنتاج الثغرة وإن وُجد X-Request-Id أضيفوا القيم. ونرجو ألا تشاركوا التفاصيل حتى نراجع البلاغ ونصححه.
