Безпека 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 | 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 також приймається.
Ключ в 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 | Час 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 | Передавайте ключ у заголовку, а не в 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 є заголовок. Вкажіть це значення в запиті до підтримки; ми одразу знайдемо ваш запит у журналі. Ніколи не вставляйте ключ чи секрет підпису в запит до підтримки, 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 зі своїм ключем підключається; компанії не бачать даних одна одної.
- У панелі 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 заповнено
- [ ] Обов'язковий підписаний запит увімкнено
- [ ] Ключ в URL (
?key=) вимкнено - [ ] Є термін дії або в календарі встановлено нагадування про оновлення
- [ ] Клієнт перевіряє сертифікат TLS (
CURLOPT_SSL_VERIFYPEERввімкнено); коли перевірку вимкнено, той, хто втрутився, може прочитати й змінити запит - [ ] Годинник сервера синхронізовано через NTP
- [ ] Підпис webhook перевіряється
- [ ] Журнал переглядається раз на місяць; ключі, до яких мали доступ співробітники, що пішли, оновлено
Повідомлення про вразливість
Якщо ви вважаєте, що знайшли вразливість в API Buluthat, зверніться через Центр підтримки в панелі «Сповіщення про безпеку» створіть запис із темою. Опишіть, як відтворити вразливість, та за наявності X-Request-Id додайте значення. Просимо вас не розголошувати подробиці, доки ми не розглянемо та не виправимо повідомлення.
