Безопасность API
API Buluthat управляет вашей телефонной АТС: запускает звонки, завершает вызовы, отправляет SMS, обрабатывает номера клиентов. Поэтому утечка ключа означает не «будет виден какой-то отчёт», а «звонки будут совершаться с вашего аккаунта». На этой странице описано, как защитить ключ и какие меры защиты Buluthat применяет за вас.
Базовый адрес: https://api.buluthat.com/api/
Уровни одним взглядом
Каждый запрос последовательно проходит через следующие шлюзы. Если один из них отклоняет запрос, он не обрабатывается и записывается в журнал.
| Порядок | Шлюз | Что делает | Ошибка |
|---|---|---|---|
| 1 | Блокировка от перебора | Если один IP за 10 мин 20 раз использует неверный ключ или подпись, он временно блокируется | 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= + подпись в шестнадцатеричном виде строчными буквами |
Каноническая строка
Подписываемая строка состоит из \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 есть заголовок. Укажите это значение в заявке в поддержку; мы сразу найдём ваш запрос в журнале. Никогда не вкладывайте ключ или секрет подписи в заявки в поддержку, письма и скриншоты.
Если вы видите незнакомый 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.
Click-to-call (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 | Не используется; приходят данные того аккаунта, которому принадлежит ключ |
- Попробуйте живой экран и click-to-call, в журнале
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 добавьте значения. Просим не делиться подробностями, пока мы не рассмотрим и не устраним уведомление.
