Безпека 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-256401 invalid_token
4Тривалість і скасуванняКлюч із простроченим терміном або скасований відхиляється401 token_expired
5Дозволені IPЯкщо для ключа визначено список IP, проходять лише запити з цих адрес403 ip_not_allowed
6Статус облікового записуКлючі закритого або неактивного облікового запису не працюють403 account_inactive
7ОбластьКлюч має доступ лише до дозволених API403 scope_denied
8ПідписЯкщо в ключі ввімкнено «обов'язковий підписаний запит», перевіряються HMAC-підпис, мітка часу та одноразовий nonce401 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 також приймається.

Ключ в 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Час 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Передавайте ключ у заголовку, а не в URL
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 є заголовок. Вкажіть це значення в запиті до підтримки; ми одразу знайдемо ваш запит у журналі. Ніколи не вставляйте ключ чи секрет підпису в запит до підтримки, e-mail або знімок екрана.

Якщо бачите запит із невідомої 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) тепер передає ключ у заголовку, а не в URL. Після оновлення CRM можна вимкнути дозвіл «Ключ в URL» у старого ключа.

Налаштування 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 заповнено
  • [ ] Обов'язковий підписаний запит увімкнено
  • [ ] Ключ в URL (?key=) вимкнено
  • [ ] Є термін дії або в календарі встановлено нагадування про оновлення
  • [ ] Клієнт перевіряє сертифікат TLS (CURLOPT_SSL_VERIFYPEER ввімкнено); коли перевірку вимкнено, той, хто втрутився, може прочитати й змінити запит
  • [ ] Годинник сервера синхронізовано через NTP
  • [ ] Підпис webhook перевіряється
  • [ ] Журнал переглядається раз на місяць; ключі, до яких мали доступ співробітники, що пішли, оновлено

Повідомлення про вразливість

Якщо ви вважаєте, що знайшли вразливість в API Buluthat, зверніться через Центр підтримки в панелі «Сповіщення про безпеку» створіть запис із темою. Опишіть, як відтворити вразливість, та за наявності X-Request-Id додайте значення. Просимо вас не розголошувати подробиці, доки ми не розглянемо та не виправимо повідомлення.