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

クイックスタート

  1. パネルで アカウントとサポート > APIキー のページを開いてください(アカウントの代表者のみ表示されます)。
  2. 新しいキー:名前を付け、必要な権限のみにチェックを入れ、サーバーの送信元IPを入力し、 署名付きリクエスト必須を開いてください。
  3. 画面に一度だけ表示される2つの値をコピーしてください: APIキー (bt_…、リクエストごとに Authorization ヘッダーで送信)および 署名シークレット (bts_…、リクエストの署名に使用されます。 送信されることはありません).
  4. 接続をお試しください:
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)
smsSMS 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分以上ずれている場合、リクエストは拒否されます。

キーで 署名付きリクエスト必須 が有効な場合、すべてのリクエストに署名が必要です。無効でも署名ヘッダーを送信した場合は署名が検証されます。誤った署名が黙って通ることはありません。

ヘッダー

ヘッダー値
AuthorizationBearer bt_…
X-Bt-TimestampUnix時刻、秒(例: 1790802088)
X-Bt-Nonceリクエストごとに新規、16-64文字 A-Z a-z 0-9 _ - (例:32 hex)
X-Bt-Signaturev1= + 署名の小文字16進表記

正規化された文字列

署名される文字列は、その間に \n (LF)の6行です:

v1
POST
/api/sms.php?action=send
1790802088
9f86d081884c7d659a2feaa0c55ad015
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
行コンテンツ
1バージョン、固定 v1
2HTTPメソッド、大文字(GET, POST)
3パスとクエリ文字列、 リクエストが送信されたままの (/api/sms.php?action=send)。ドメイン名とスキーマは含まれません
4X-Bt-Timestamp 値
5X-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コードをプレーンテキストのメッセージで返します。

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" }

レート制限

制限デフォルト
キーごとに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日ごと、担当者の退職時、または漏えいの疑いがある場合に更新してください。無停止での切り替え:

  1. APIキー > 該当のキー > 設定 > キーを更新。「旧キーは24時間有効」を選択してください。
  2. 同じ設定で新しいキーと新しい署名シークレットが生成され、画面に一度だけ表示されます。
  3. 新しい値で連携を更新してください。
  4. ログに旧キーのプレフィックス(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に 専用のキーで に紐づけられ、各社は互いのデータを見ることができません。

  1. Buluthatのパネルで、会社のアカウントで APIキー > 新しいキー:権限 リアルタイム交換機データ, 通話制御と列, 自動発信 (使用している場合 音声アシスタント, 音声確認コード)。ByCRMサーバーのIPを入力してください。
  2. ByCRM > 連携 > Buluthat:
エリア値
Buluthat APIトークンbt_…
署名シークレット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分あたり1.200)。ログにはエラーとなったブリッジリクエストのみが記録されます。

Webhookの検証

Buluthatから送信されるWebhookにも署名が付いています(X-Buluthat-Signature: sha256=…)。サーバー側で署名を 生の本文 での検証なしに、Webhookを処理しないでください。 X-Buluthat-Delivery と同じ配信を2回処理しないでください。詳細: Webhook.

セキュリティチェックリスト

  • [ ] 連携ごとに専用のキーがあり、必要なスコープのみ
  • [ ] キーはコードではなく環境変数またはシークレット管理に(.env Gitには含まれません)
  • [ ] キーはサーバー側のみ。ブラウザのJavaScript、モバイルアプリ、Excelマクロには存在しない
  • [ ] 許可IPリストが入力されている
  • [ ] 署名付きリクエスト必須が有効
  • [ ] URLでのキー(?key=)オフ
  • [ ] 有効期限があるか、カレンダーに更新のリマインドが設定されている
  • [ ] クライアントがTLS証明書を検証している(CURLOPT_SSL_VERIFYPEER 有効)。検証が無効だと、途中の第三者がリクエストを読み取り、改ざんできます
  • [ ] サーバー時刻がNTPで同期されている
  • [ ] Webhookの署名を検証している
  • [ ] ログを月に1回見直している。退職したスタッフがアクセスできたキーは更新済み

脆弱性の報告

Buluthat APIにセキュリティ上の脆弱性を見つけた場合は、パネル内のサポートセンターから 「セキュリティ通知」 の件名で記録を作成してください。脆弱性をどのように再現できたか、また可能であれば X-Request-Id の値を追加してください。通知を確認し修正するまで、詳細を共有しないようお願いします。