APIセキュリティ
Buluthat APIはお客様の電話交換機を操作します:発信、通話の切断、SMS送信、お客様の番号の処理。そのためキーが漏えいした場合、「レポートが見られる」ではなく「お客様のアカウントから発信される」ことを意味します。このページでは、キーの保護方法と、Buluthatがお客様に代わって適用している保護について説明します。
基本アドレス: https://api.buluthat.com/api/
レイヤーの概要
リクエストは順番に次の関門を通過します。どれか1つで拒否されると、リクエストは処理されず、ログに記録されます。
| 順序 | 関門 | できること | エラー |
|---|---|---|---|
| 1 | ブルートフォースロック | 1つのIPが10分間に20回誤ったキーまたは署名を試行すると、そのIPは一時的にロックされます | 429 ip_locked |
| 2 | 本文の上限 | 5 MBを超えるリクエスト本文は読み取られません | 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署名、タイムスタンプ、使い捨てノンスが検証されます | 401 signature_* |
| 9 | レート制限 | キーごと、およびアカウント合計の1分あたり制限 | 429 rate_limited |
クイックスタート
- パネルで アカウントとサポート > APIキー のページを開いてください(アカウントの代表者のみ表示されます)。
- 新しいキー:名前を付け、必要な権限のみにチェックを入れ、サーバーの送信元IPを入力し、 署名付きリクエスト必須を開いてください。
- 画面に一度だけ表示される2つの値をコピーしてください: APIキー (
bt_…、リクエストごとにAuthorizationヘッダーで送信)および 署名シークレット (bts_…、リクエストの署名に使用されます。 送信されることはありません). - 接続をお試しください:
curl "https://api.buluthat.com/api/autocall.php?action=ping" \
-H "Authorization: Bearer bt_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
署名が必須の場合、このリクエストは 401 signature_required を返します。以下の リクエスト署名 セクションの例のいずれかをご利用ください。
キーと署名シークレットはシステム内で読み出し可能な形では 保存されません。紛失した場合は復元できません。パネルからキーを再発行してください。
スコープ
各キーは1つまたは複数のスコープ付きで生成されます。スコープ外の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。本当に必要な場合のみ |
原則: 連携ごとに別のキー、各キーに最小限の権限を。ECサイトのSMSキーが漏えいしても、攻撃者は発信を開始できません。そのキーだけを無効化すれば済みます。
キーを送信しない
キー ヘッダーで 送信されます:
Authorization: Bearer bt_xxxxxxxx
Authorization ヘッダーを設定できない環境向けに X-Api-Key: bt_xxxxxxxx も受け付けられます。
URLでのキー(?key=)
?key=bt_… 新しいキーでは は無効です と 401 query_key_disabled を返します。URLはWebサーバーのログ、プロキシの記録、ブラウザの履歴、および Referer ヘッダーに記録され、キーはそこから漏れます。ヘッダーを送れない古いシステム向けにのみ、キー設定で 「キーをURLで受け付ける」 を開くことができます。パネルはこれらのキーを赤で ?key= というバッジで表示します。
V54より前に生成されたキーでは、従来の連携が途切れないよう、この権限を有効のままにしています。連携をヘッダー方式に移行したら無効にしてください。
許可IP
キー設定の 許可IPアドレス 欄に1行につき1つのIPまたはCIDRブロックを入力します:
85.111.89.220
10.20.0.0/24
2a01:4f8:c0c:1234::/64
リストが空の場合はすべてのIPを受け付けます。入力済みの場合、リスト外のアドレスからのリクエストは、キーが正しくても 403 ip_not_allowed を返します。書き込むアドレスは、APIを呼び出す はお使いのサーバーの送信元IPです (お使いのパソコンのものではありません)。ご不明な場合は、ログでそのサーバーからのリクエストのIP列をご確認ください。
リクエスト署名
署名により、キーが漏えいしてもリクエストを送れなくなります。攻撃者には署名シークレットも必要で、このシークレットはどのリクエストでもネットワーク上に流れません。署名には次の効果もあります:
- 本文をロックします: パスの1文字でも変わると署名は一致しません。
- 再生の重複を防ぎます: 各ノンスは1回のみ受け付けられるため、傍受されたリクエストを2回目に送信することはできません。
- 古いリクエストを拒否します: タイムスタンプがサーバー時刻から±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= + 署名の小文字16進表記 |
正規化された文字列
署名される文字列は、その間に \n (LF)の6行です:
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ダイジェスト、小文字の16進数。本文のないリクエストおよび 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 4番目のパラメータで ['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を 1回 生成し、同じ変数を要約とリクエストの両方に渡してください |
signature_invalid トルコ語の文字を含む照会で | パスをご自身で組み立て、別のエンコードをされています。署名には、クライアントが実際に送信したURLのパスとクエリを使用してください(parse_url / new URL()) |
signature_expired | サーバーの時刻がずれています。NTP(timedatectl set-ntp true)を開きます。許容誤差は±300秒 |
signature_replayed | 同じノンスが2回送信されました。再試行(リトライ)時はノンスとタイムスタンプを を再生成して 再署名してください |
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" }
レート制限
| 制限 | デフォルト |
|---|---|
| キーごとに | 1分あたり120リクエスト(キー設定で下げられます) |
| アカウントのすべてのキーの合計 | 1分あたり600リクエスト |
担当者のステータス(agent_statuses) | さらにアカウントごとに1分あたり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に2つの別々の認証情報で接続します:
| 設定(CRM) | 値 |
|---|---|
| 設定 > 自動発信 > APIキー | Buluthatで生成された bt_… キー(スコープ: autocall + voicebot + voice_otp + call) |
| 設定 > 自動発信 > 署名シークレット | 同じキーの bts_… のシークレット。入力されている場合、CRMからBuluthatへのすべてのリクエストに署名されます |
| VoIP設定 > Buluthat APIトークン | リアルタイム画面 / CDRブリッジ(crm_bridge)。Buluthatチームが発行します。上記のキーとは別です |
推奨する順序:キーを 署名付きリクエスト必須 オフで生成し、CRMにキーとシークレットを入力し、ログでリクエストの imzalı というバッジで届くことを確認してから、キーで署名を必須にしてください。キーにはCRMサーバーのIPも追加してください。
クリックで発信(begin_call)はキーをURLではなくヘッダーで送信するようになりました。CRMの更新後、旧キーの「URLでのキー」許可をオフにできます。
ByCRMの設定
ByCRMでは各社がBuluthatに 専用のキーで に紐づけられ、各社は互いのデータを見ることができません。
- Buluthatのパネルで、会社のアカウントで APIキー > 新しいキー:権限 リアルタイム交換機データ, 通話制御と列, 自動発信 (使用している場合 音声アシスタント, 音声確認コード)。ByCRMサーバーのIPを入力してください。
- ByCRM > 連携 > Buluthat:
| エリア | 値 |
|---|---|
| Buluthat APIトークン | 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分あたり1.200)。ログにはエラーとなったブリッジリクエストのみが記録されます。
Webhookの検証
Buluthatから送信されるWebhookにも署名が付いています(X-Buluthat-Signature: sha256=…)。サーバー側で署名を 生の本文 での検証なしに、Webhookを処理しないでください。 X-Buluthat-Delivery と同じ配信を2回処理しないでください。詳細: Webhook.
セキュリティチェックリスト
- [ ] 連携ごとに専用のキーがあり、必要なスコープのみ
- [ ] キーはコードではなく環境変数またはシークレット管理に(
.envGitには含まれません) - [ ] キーはサーバー側のみ。ブラウザのJavaScript、モバイルアプリ、Excelマクロには存在しない
- [ ] 許可IPリストが入力されている
- [ ] 署名付きリクエスト必須が有効
- [ ] URLでのキー(
?key=)オフ - [ ] 有効期限があるか、カレンダーに更新のリマインドが設定されている
- [ ] クライアントがTLS証明書を検証している(
CURLOPT_SSL_VERIFYPEER有効)。検証が無効だと、途中の第三者がリクエストを読み取り、改ざんできます - [ ] サーバー時刻がNTPで同期されている
- [ ] Webhookの署名を検証している
- [ ] ログを月に1回見直している。退職したスタッフがアクセスできたキーは更新済み
脆弱性の報告
Buluthat APIにセキュリティ上の脆弱性を見つけた場合は、パネル内のサポートセンターから 「セキュリティ通知」 の件名で記録を作成してください。脆弱性をどのように再現できたか、また可能であれば X-Request-Id の値を追加してください。通知を確認し修正するまで、詳細を共有しないようお願いします。
