أمان 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-256401 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)
smsSMS 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 دقائق.

في المفتاح الطلب الموقّع إلزامي إذا كان مفعّلًا فيجب أن يكون كل طلب موقّعًا. وحتى لو كان مغلقًا فإذا أرسلتم ترويسات التوقيع يُتحقق من التوقيع؛ والتوقيع الخاطئ لا يمر بصمت.

الترويسات

العنوانالقيمة
AuthorizationBearer bt_…
X-Bt-Timestampوقت Unix بالثواني (مثال: 1790802088)
X-Bt-Nonceجديد في كل طلب، 16-64 حرفًا A-Z a-z 0-9 _ - (مثال: 32 hex)
X-Bt-Signaturev1= + التوقيع بصيغة 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). لا يشمل اسم النطاق والمخطط
4X-Bt-Timestamp القيمة
5X-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 نفسه برسالة نصية عادية.

HTTPcodeماذا يجب فعله
401missing_tokenAuthorization: Bearer … أضيفوا الترويسة
401invalid_tokenالمفتاح خاطئ أو ملغى أو غير موجود. لا تعيدوا المحاولة، صحّح الإعداد
401token_expiredجدّدوا المفتاح من اللوحة
401query_key_disabledأرسل المفتاح في الترويسة بدل الرابط
401signature_*انظروا الجدول أعلاه
403ip_not_allowedأضيفوا عنوان IP الصادر لخادمكم إلى قائمة المفتاح
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 > Buluthat API Tokenجسر الشاشة المباشرة / CDR (crm_bridge); يقدمه فريق Buluthat وهو منفصل عن المفتاح أعلاه

الترتيب الموصى به: المفتاح الطلب الموقّع إلزامي أنشئوه مغلقًا، وأدخلوا المفتاح والسر في CRM، وفي السجل الطلبات imzalı شاهدوا أنها تصل بشارة، ثم اجعلوا التوقيع إلزاميًا في المفتاح. وأضيفوا إلى المفتاح عنوان IP لخادم CRM أيضًا.

النقر للاتصال (begin_call) لم يعد يرسل المفتاح في الرابط بل في الترويسة. بعد تحديث CRM يمكنكم إغلاق إذن "المفتاح في الرابط" في المفتاح القديم.

إعداد ByCRM

في ByCRM كل شركة إلى Buluthat بمفتاحه الخاص يرتبط؛ ولا ترى الشركات بيانات بعضها البعض.

  1. في لوحة Buluthat بحساب الشركة مفاتيح API > مفتاح جديد: الصلاحيات بيانات السنترال المباشرة, التحكم بالمكالمات والطوابير, الاتصال الآلي (إن كان مستخدمًا المساعد الصوتي, رمز التحقق الصوتي). اكتب عنوان IP لخادم ByCRM.
  2. ByCRM > التكاملات > Buluthat:
المنطقةالقيمة
Buluthat API Tokenbt_…
سر التوقيع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 المسموحة ممتلئة
  • [ ] "الطلب الموقّع إلزامي" مفعّل
  • [ ] المفتاح في الرابط (?key=) مغلق
  • [ ] له تاريخ انتهاء أو ضُبط تذكير بالتجديد في التقويم
  • [ ] يتحقق العميل من شهادة TLS (CURLOPT_SSL_VERIFYPEER مفتوح)؛ وإذا كان التحقق مغلقًا يمكن لمن يعترض الاتصال أن يقرأ الطلب ويغيّره
  • [ ] ساعة الخادم مضبوطة بـ NTP
  • [ ] يُتحقق من توقيع Webhook
  • [ ] يُراجَع السجل شهريًا؛ وجُدّدت المفاتيح التي وصل إليها الموظفون المغادرون

الإبلاغ عن ثغرة أمنية

إن ظننتم أنكم عثرتم على ثغرة أمنية في Buluthat API فمن مركز الدعم داخل اللوحة "إشعار أمني" افتحوا سجلًا بموضوع. واكتبوا كيف أعدتم إنتاج الثغرة وإن وُجد X-Request-Id أضيفوا القيم. ونرجو ألا تشاركوا التفاصيل حتى نراجع البلاغ ونصححه.