ボット開発
音声ボット
アプリからボットに電話をかけると、AI 音声モデルが応答します。有効にする方法、指示文と声、通話の通知、ツール、MCP サーバー、自前のモデルキー、文字起こし、ストリームモード、Apple Watch と会話を残す機能まで — ボットのオーナー向けの案内です。
音声ボットは、アプリから人が電話をかけられるボットです。電話に出るのはボットのオーナーのコードではなく、Pabal の AI 音声モデルです。ボットのオーナーは、そのモデルが従う指示文・あいさつ・声・言語・上限を Bot API で決めます。通話が始まるときと終わるときに voice_call アップデートを受け取り、通話中にモデルからボットのコード(ツール)を呼ばせて、注文を調べたり予約を確認したりできます。AI モデルの代わりに自分のサーバーが音声を直接受け取るストリームモードもあります。初めてボットを作る場合は、先にボットを作るをご覧ください。
現在の状況
下の条件がすべてそろったサーバーでだけ、音声ボットに電話できます。メソッドは音声が無効なサーバーにもあるので、設定を読むことはできます。ただし、このサーバーで音声が有効になり、運営者がそのボットを許可するまでは、無効にする操作(音声を無効にする・文字起こしを無効にする・ストリームモードを無効にする・自前のキーを削除する・MCP サーバーを削除する)以外の設定は保存されません(403 VOICE_NOT_AVAILABLE)。自分のボットがいま電話を受けられるかどうかは、getVoiceCallSettings の available と unavailable_reason でわかります。
| 機能 | 有効になる条件(すべて) | pabal.me の現状 |
|---|---|---|
| 音声ボットの通話 | サーバーに音声ゲートウェイと通話中継(TURN)が付いている · プライバシーポリシーにボット通話の節(bot-calls)が掲載されている · 運営者がそのボットを許可している · 一般公開の前は運営者が決めたテスターだけが電話できる · 発信者のアプリが確認画面を含むビルド(デスクトップ pabal-bc2 — 確認画面が韓国語・英語だけの間は pabal-bc1 も受け付ける、Android 62151、iPhone ビルド 5 以上、Apple Watch アプリのビルド 1 以上)である — そのようなビルドはまだ配布されていません | 無効 |
| Apple Watch からかける | 音声ボットの通話 + サーバーのウォッチ API + ウォッチの音声のエッジ(pabal-voice-edge)+ 直通通話(direct)を受け付けるゲートウェイ · ウォッチアプリのビルドが TELEGRAM_VOICE_MIN_BUILD_WATCHOS 以上 · ツール・MCP サーバー・自前のキー・ストリームモード・ボットのオーナーの文字起こしを使うボットはまだ不可 | 無効 — 開発サーバーで試験中 |
| 会話を残す(発信者の写し) | 音声ボットの通話 + ポリシーの bot-calls-keep-text 節 + keep_text を示したゲートウェイ(0.9.0)+ チェックボックスを含むアプリのビルド · 確認画面の文言の法的な確認 | 無効 — 法的な確認の前 |
| ボットからの電話 | プライバシーポリシーの bot-calls-outbound 節と利用規約の voice-bot-owners-outbound 節が掲載されている(ボットオーナーが規約の第 4 版に同意するには voice-bot-owners 節も)· 法的な確認のあとに運営者がオンにする全体のスイッチ · ボットオーナーが @BotFather で有効にする(規約の第 4 版・目的)· 運営者による発信の審査 · 着信者の許可(許可画面の入ったアプリのビルド)· 承認済みの発信の案内録音を持つゲートウェイ · ボットの言語が韓国語・英語 | 無効 — 法的な確認待ち |
| 韓国語・英語以外のボットの言語(7 言語) | 音声ボットの通話 + そのボットの言語で通話するゲートウェイ(0.7.0 からは 9 言語すべて) · 運営者がそのボットの指示文・あいさつを読んで許可している(すべてのボットを受け付けるサーバーでも) | 無効 — デプロイ前 |
| 韓国語・英語以外の確認画面と案内音声(8 言語) | 言語ごとに法的な確認とネイティブスピーカーによる確認を終えたあとで、アプリのビルドとゲートウェイのイメージに載る。それまでは、その言語のアプリを使う発信者も英語の確認画面を見て、英語の案内を聞く | 草案 — 確認前 |
| ボットオーナー規約(第 4 版) | 利用規約にボットオーナーの節(voice-bot-owners)が掲載されている。文字起こし・自前のキー・ストリームモード・MCP サーバー・ボットからの電話は、どれもまずこの規約が必要です | 同意できない |
| ツール | 音声ボットの通話 + ツールを受け付けるゲートウェイ | 無効 |
| MCP サーバー | MCP の部品 · 専用の送信元アドレス(運用上の条件 — コードは確認しない) · ポリシーの bot-calls-mcp 節と規約のボットオーナー節 · ボットオーナー規約への同意 · ボットの MCP の送り先を確認画面に表示するアプリのビルド(それまでは登録はできても、どの通話にも MCP ツールは載らない) | 無効 |
| 自前のモデルキー | サーバーのマスターキー · 自前のキーを使うボットの送り先を表示する確認画面(第 2 版)をアプリが出したあとで運営者が有効にする · ボットオーナー規約 · オーナーのキーで動かせるゲートウェイ(既定では Google のキーのみ) | 無効 — キーを受け付けもしない |
| 文字起こし | 運営者が法的な確認のあと管理画面で許可する(既定は無効) · ボットオーナー規約 · ボットのオーナーのスイッチ · 文字起こしの案内音声を持つゲートウェイ · マネージド通話 | 無効 |
| ストリームモード | サーバーのマスターキー · ポリシーの bot-calls-stream 節と規約のボットオーナー節 · ストリーム通話を受け付けるゲートウェイ · ストリーム用の確認画面を含むアプリのビルド(まだない) | 無効 |
- 発信者(電話をかける人)は 19 歳以上でなければならず、電話をかける前に「An AI answers this call」(この通話には AI が応答します)という確認画面を見て、つながると最初に AI の案内音声を聞きます(発信者に見えるもの)。確認画面のないアプリ(Telegram の元のアプリや Telethon のようなクライアントを含む)では、ボットに通話ボタンが表示されません。
- マネージド通話の音声は Google に送られます — Vertex AI の Gemini Live API、米国アイオワ(Google Cloud
us-central1)。Pabal は音声を保存しません。Google は返答を作ったあと音声も文字も保管しませんが、自動検査で不正利用が疑われた内容は最大 90 日間記録されることがあります(確認画面と同じ内容)。本番のエンジンはこれ 1 つで、ほかの提供元にひそかに切り替えることはありません(自動切り替えなし)。 - ボットとの通話は人どうしの通話と違い、エンドツーエンド暗号化ではありません。Pabal の音声ゲートウェイが音声を復号して AI モデルに渡します。発信者の確認画面にもそう表示されます。
1 本の電話の流れ
図の説明
- 3 つの領域:発信者のアプリ → Pabal 運営者のサーバー(Pabal サーバー、音声ゲートウェイ、中継サーバー) → Pabal の外(Google の AI モデル、ボットのオーナーのプログラム)。ボットのオーナーのコードは、音声を 1 バイトも見ません。
- ① 電話:アプリは確認画面で得た同意をサーバーに伝えてから電話をかけます。サーバーは発信者(同意 — 19 歳以上であることの自己申告 · アプリのビルド · テスター · 上限)とボット(許可 · 設定 · 上限)をすべて確認し、電話を受けるか、話し中で終えます。
- ② リンク:サーバーは通話を音声ゲートウェイに渡し、ボットのオーナーの指示文・あいさつ・声・言語・ツールも一緒に送ります。ゲートウェイは Pabal の基本ルール(AI であると名乗るなど)を常に先に置き、その後ろに指示文を置きます。
- ③ Bot API:ボットのオーナーのプログラムは設定を決め、通話の開始・終了時に
voice_callを、モデルがツールを呼ぶとvoice_call_tool_queryを受け取って答えます。③ の青い矢印はサーバーからボットへ送るもの、その下の灰色の矢印はボットからサーバーへ送るものです。 - ④ 音声(下の段):アプリとゲートウェイはお互いのアドレスを知らないまま、中継サーバー(coturn)だけを通して音声をやり取りし、ゲートウェイが音声を Google のモデルに渡して返答を受け取ります。赤い枠が、音声が Pabal の外へ出る場所です。
- ストリームモードでは、④ の Google の位置にボットのオーナーのサーバーが入ります(ストリームモード)。
Apple Watch からかける通話(準備中)
ウォッチアプリは MTProto を使いません。ウォッチはサーバーのウォッチ API(HTTPS JSON)でログインし、ボットの一覧と確認画面を受け取って電話をかけます。サーバーはアプリの電話と同じ受付の判定を行ったあと、その通話にだけ使える1 回限りのチケット(15 秒)をウォッチに渡し、通話をゲートウェイに渡します。ウォッチはそのチケットを持って音声のエッジ(WSS、pabal-voice-edge)に接続し、エッジがその通話を担当するゲートウェイのワーカーに音声をつなぎます。鍵交換(DH)も通話中継(coturn)もありません。それでもゲートウェイが音声を復号してモデルに渡すことは同じなので、エンドツーエンド暗号化ではなく、音声の送り先も同じです。ボットのオーナーには、アプリの通話とまったく同じ voice_call が届きます(started のタイミングだけが異なる、下記)。
音声通話を有効にする
音声通話を有効にするのは、@BotFather で人が行います — 音声ボットのルールへの同意は、トークンではなくボットのオーナーの約束だからです。それ以外の設定は、@BotFather と Bot API のどちらでもできます。
- 運営者にボットの許可を頼みます。許可したボットだけを受け付けるサーバー(既定)では、運営者が管理画面の音声タブでボット ID(トークンの
:の前の数字)を許可するまで、@BotFather に音声通話のボタンがなく、設定を変えると403 VOICE_NOT_AVAILABLEになります。一般公開の前なら、試しに電話をかける自分のアカウントもテスターに入れてもらってください(自分のユーザー ID は、ボットにメッセージを送ったあとのgetUpdatesのmessage.from.id)。 - 指示文を決めます。ボットが電話で何をするかを書いた文章です。下の
setVoiceCallSettingsで決めるか、@BotFather の音声通話の画面で指示文(지시문)ボタンから送ります。指示文がないと有効にできません(ストリームモードは例外)。 - @BotFather で
/mybots→ ボット → 音声通話(음성 통화) → オンにする(켜기)。最初の 1 回は音声ボットのルールが表示され、ルールに同意してオンにする(규칙에 동의하고 켜기)を押すと有効になります。次からは「オンにしますか?」(켤까요?)と聞かれるだけです。 - 再許可を待ちます。許可したボットだけを受け付けるサーバーでは、許可はその時点の指示文・あいさつ・ツールを許可したことになるため、許可のあとで指示文を書くと
unavailable_reason: "awaiting_review"になります。運営者が確認して再び許可すると、電話を受けられるようになります。3 つのうちどれか(ストリームモードならストリームのアドレス)を変えるたびに同じです。ボットの言語が韓国語・英語以外なら、すべてのボットを受け付けるサーバーでも、運営者が指示文・あいさつを読んで許可するまで電話を受けません(声と言語)。 getVoiceCallSettingsのavailableがtrueであることを確かめ、確認画面を含むアプリのビルドで、ボットのプロフィールの通話ボタンから電話してみます。
BotFather は韓国語で返信します。上のルールの内容は次のとおりです。1. ボットは常に AI であると名乗り、人のふりをさせることはできない。2. 実在の機関・会社・人になりすますことはできない。3. カード番号・住民登録番号・パスワード・認証番号を尋ねさせることはできない。4. 通話の音声は Pabal が保存せず、返答を作るために Google(Vertex AI)に送られる。5. ボットとの音声通話は 19 歳以上の人だけができる。6. ルールに違反すると、運営者が音声通話やボットを停止できる。同意すると、BotFather は音声通話を有効にしたことを伝え、ボットの音声通話の状態(状態:オン、声:Kore(しっかりした)、言語:韓国語)を表示し、電話に出るとまず「You're now talking to an AI voice bot.」(AI 音声ボットにつながりました)の案内が流れ、そのあとボットがあいさつすると知らせます。
許可したボットだけを受け付けるサーバーでは、状態が "켜짐 — 바뀐 지시문·인사말을 운영자가 확인할 때까지 전화를 받지 않아요"(「オン — 変更された指示文・あいさつを運営者が確認するまで電話を受けません」)と表示されることがあります(上の手順 4)。ボットの言語が韓国語・英語以外なら、どのサーバーでも運営者が許可するまで "켜짐 — 한국어·영어가 아닌 봇은 운영자가 지시문·인사말을 읽고 허용할 때까지 전화를 받지 않아요 (자동 규칙 검사가 한국어와 영어만 읽어요)"(「オン — 韓国語・英語以外のボットは、運営者が指示文・あいさつを読んで許可するまで電話を受けません(自動ルール検査は韓国語と英語しか読めません)」)と表示されます。
音声通話の画面には、オン・オフ、指示文、あいさつ、声、言語、上限、使用量、文字起こし、自前のキー、ストリームモード、(MCP が有効なサーバーでは)MCP サーバーのボタンがあります。音声が無効になったり許可が外れたりしたあとでも、無効にする操作と削除はいつでもできます — 音声を無効にする、自前のキーを削除する、ストリームモードを無効にする、MCP サーバーを削除する。
有効にするときと通話のたびに、ボットの名前もなりすましの検査を受けます。実在の機関・会社の名前を使ったボットは VOICE_CONTENT_REFUSED: name: impersonation で有効になりません。その機関のボットであれば、サーバーのサポート窓口に連絡してください。
設定 — setVoiceCallSettings
ボットの音声の設定を決めます。送った項目だけが変わる部分更新で、全体を検査してから一度に書き込むか、すべてを拒否します。何も変わっていなければ何も書き込みません。
curl -s "https://pabal.me/bot$BOT_TOKEN/setVoiceCallSettings" -H 'Content-Type: application/json' -d '{
"instructions": "あなたは注文ヘルパー店の電話窓口です。丁寧に短く答えてください。営業時間は午前10時から午後8時までです。",
"greeting": "注文ヘルパーです。ご用件をどうぞ。",
"voice": "kore",
"language": "ko-KR",
"max_call_duration": 180
}'
# {"ok":true,"result":true}
| パラメーター | 型 | 説明 |
|---|---|---|
enabled | Boolean | オン・オフ。オンにするには @BotFather でルールに同意済みである必要があり(そうでなければ 403 VOICE_RULES_NOT_ACCEPTED)、マネージドなら指示文が必要です。オフにするのはいつでもできます — いま進行中の通話は最後まで続き、新しい電話だけを受けなくなります |
instructions | String | モデルが従う指示文。UTF-8 で 8,192 バイト(日本語で約 2,700 文字)まで。"" は削除 — 有効なマネージドモードのボットでは空にできません。長いほど通話の料金が増え、発信者に知られる可能性があるので、パスワードやキーは入れないでください |
greeting | String | AI の案内のあとにボットが最初に言う言葉。512 バイト(日本語で約 170 文字)まで。"" ならモデルが自分で短くあいさつします |
voice | String | 声の id(一覧)、既定は kore |
language | String | ボットが話し、聞き取る言語で、9 つの id のどれかです(既定は ko-KR、一覧)。zh-CN は cmn-CN として、大文字・小文字だけが違う id(ja-jp)は一覧の表記で保存します。韓国語・英語以外なら、運営者が指示文・あいさつを読んで許可したあとで電話を受けます。冒頭の AI の案内音声はこれではなく、発信者が見た確認画面の言語で流れます |
max_call_duration | Integer | 1 回の通話の最大の長さ(秒)。60 以上、limits.max_call_duration(運営者とサーバーの上限のうち小さいほう、既定 600)以下。ボットの既定値 300 |
max_concurrent_calls | Integer | 同時に受ける通話の数。1 以上、limits.max_concurrent_calls(既定 2)以下。ボットの既定値 2 |
tools | Array | 通話中にモデルが呼べるボットのツール(ツール)。[] ですべて削除 |
transcript | Boolean | 文字起こしのスイッチ(文字起こし) |
mode、stream_url、stream_secret | String | ストリームモード(ストリームモードの設定) |
model、api_key | 受け付けない — VOICE_FIELD_NOT_AVAILABLE。モデルはサーバーが決め、キーは setVoiceCallApiKey でのみ |
- 内容のルール:指示文・あいさつ・ツールの説明は、保存するときと通話のときに検査されます。人間だと言わせる(
claims_human)、AI であることを隠させる(hides_ai)、Pabal のルールを無視させる(overrides_rules)、実在の機関・会社へのなりすまし(impersonation)、公的機関だと名乗る(official_claim)、カード番号・住民登録番号・パスワード・認証番号を尋ねる(asks_secrets)、秘密鍵のように見える文字列(looks_like_secret)、見えない文字(hidden_text)はVOICE_CONTENT_REFUSED: <項目>: <コード>で拒否されます。「パスワードは絶対に聞かないでください」のような否定の文は通ります。 - Pabal の基本ルールが常に優先されます。モデルは通話のたびに、Pabal のルール → ボットのオーナーの指示文 → Pabal のルールの結び、の順に読み、指示文は「ボットのオーナーの文章」と印を付けた枠の中に入ります。指示文は Pabal のルールの代わりになったり、それより優先されたりすることはできません。
- 変更は1 時間に 30 回までです(@BotFather と合わせて、実際に変わったものだけを数える)。超えると
429 Too Many Requests: retry after Nとparameters.retry_afterが返ります。 - フォーム(
-d name=value)で送る場合、toolsは JSON 文字列で入れます。 - MTProto だけでログインしたボットは、同じ JSON を
bots.sendCustomRequestで送ります(MTProto のボット)。
現在の設定を見る — getVoiceCallSettings
curl -s "https://pabal.me/bot$BOT_TOKEN/getVoiceCallSettings"
{
"ok": true,
"result": {
"enabled": true,
"available": true,
"suspended": false,
"rules_accepted": true,
"instructions": "あなたは注文ヘルパー店の電話窓口です。…",
"greeting": "注文ヘルパーです。ご用件をどうぞ。",
"voice": "kore",
"language": "ko-KR",
"max_call_duration": 180,
"max_concurrent_calls": 2,
"limits": {"max_call_duration": 600, "max_concurrent_calls": 2, "bot_minutes_per_day": 60, "owner_minutes_per_day": 120},
"voices": [{"id": "zephyr", "name": "Zephyr", "description": "Bright"}, …],
"languages": ["ko-KR", "en-US", "ja-JP", "cmn-CN", "fr-FR", "es-US", "id-ID", "vi-VN", "ar-EG"],
"testers_only": true,
"mode": "managed",
"has_stream_secret": false,
"owner_terms_accepted": false,
"stream_available": false,
"updated_date": 1790380712,
"tools": [],
"tool_limits": {"max_tools": 16, "max_definition_bytes": 8192, "definition_bytes": 0, "mcp_tools": 0},
"tool_delivery": "none",
"transcript": false,
"transcript_available": false,
"transcript_delivery": "get_updates",
"has_api_key": false,
"own_keys_available": false,
"mcp_available": false
}
}
| フィールド | 意味 |
|---|---|
enabled | ボットのオーナーが有効にしたかどうか |
available、unavailable_reason | 誰がかけても、いまボットが電話を受けられるかどうかと、受けられないならその理由(下の表) |
rules_accepted、owner_terms_accepted | 音声ボットのルール、現在のボットオーナー規約(第 4 版)に同意したかどうか |
limits | このサーバーで設定できる最大値と 1 日の上限(分) |
voices、languages | 選べる声と言語 |
testers_only | 一般公開の前で、運営者が決めたテスターだけが電話できるなら true |
tools、tool_limits、tool_delivery、tool_delivery_problem | ツールとその上限、ツール問い合わせがいま届く先(webhook・get_updates・mtproto・none)と、届かない理由(ツール問い合わせを受け取る) |
mode、stream_url、stream_secret_fingerprint、has_stream_secret、stream_available、stream_ignores | ストリームモード — シークレットは表示されず、8 文字のフィンガープリントだけ |
transcript、transcript_available、transcript_unavailable_reason、transcript_delivery、transcript_delivery_problem | 文字起こしのスイッチと、いま実際に届けられるかどうか |
has_api_key、api_key、own_keys_available | 自前のモデルキー — キーは表示されず、提供元・フィンガープリント・設定日・使えるかどうかだけ |
mcp_available、mcp_servers、mcp_unavailable_reason | MCP サーバー |
outbound | ボットからの電話の状態(ボットからの電話 — 設定と使用量) |
updated_date | 最後に変更した時刻(Unix 秒) |
unavailable_reason | 意味 |
|---|---|
voice_off | このサーバーで音声が無効か、プライバシーポリシーのボット通話の節がまだない |
kill_switch | 運営者がすべての音声通話を止めた |
bot_voice_off | ボットのオーナーが有効にしていない |
not_allowed | 運営者がこのボットを許可していない |
awaiting_review | 変更された指示文・あいさつ・ツール(ストリームモードならストリームのアドレス)を、運営者がまだ確認していない。ボットの言語が韓国語・英語以外なら、すべてのボットを受け付けるサーバーでも、運営者が指示文・あいさつを読んで許可する必要がある |
suspended | 運営者がこのボット(またはオーナーのアカウント)の音声を停止した |
name_refused、text_refused | ボットの名前、または保存された指示文・あいさつ・ツールの説明が、現在の内容のルールに合わない。text_refused は、ボットが選んだ言語が一覧から外れて使えなくなったときにも出ます — 言語を選び直してください |
no_relay、no_gateway | サーバーに通話中継または音声ゲートウェイがない |
no_gateway_with_tools、no_own_key_gateway | このボットのツールや自前のキーを使えるゲートウェイがない |
no_gateway_with_language | このボットの言語で通話するゲートウェイが接続されていない(声と言語) |
own_key_unavailable、stream_unavailable | 設定した自前のキーやストリームモードがいま使えないので、電話を受けない(プラットフォームのキーで代わりに受けることはしません) |
声と言語
Gemini Live のあらかじめ用意された 30 の声から選びます(既定は kore)。声をアップロードしたり複製したりする機能はありません。@BotFather の声(목소리)ボタンでは、名前と雰囲気で選びます。
| id | 雰囲気 | id | 雰囲気 | id | 雰囲気 |
|---|---|---|---|---|---|
zephyr | 明るい | puck | 軽快な | charon | はっきりした |
kore | しっかりした | fenrir | 弾んだ | leda | 若々しい |
orus | しっかりした | aoede | さわやかな | callirrhoe | のんびりした |
autonoe | 明るい | enceladus | 息まじりの | iapetus | 澄んだ |
umbriel | のんびりした | algieba | なめらかな | despina | なめらかな |
erinome | 澄んだ | algenib | かすれた | rasalgethi | はっきりした |
laomedeia | 軽快な | achernar | やわらかな | alnilam | しっかりした |
schedar | 穏やかな | gacrux | 大人びた | pulcherrima | 積極的な |
achird | 親しみやすい | zubenelgenubi | 気さくな | vindemiatrix | やさしい |
sadachbia | 生き生きした | sadaltager | 博識な | sulafat | 温かい |
言語(language)は下の 9 つのどれかです。ボットが話し、聞き取る言語で、ボットはこの言語で答えます。@BotFather では、音声通話の画面の言語(언어)ボタンで選びます。
| id | 言語 | 備考 |
|---|---|---|
ko-KR | 韓国語 | 既定 |
en-US | 英語 | — |
ja-JP | 日本語 | — |
cmn-CN | 中国語(普通話) | Google の公式サポート一覧に含まれていないため、品質は Google が保証していません(テストでは聞き取りも返答も正確でした)。zh-CN で送っても受け付け、cmn-CN として保存します |
fr-FR | フランス語 | — |
es-US | スペイン語 | — |
id-ID | インドネシア語 | — |
vi-VN | ベトナム語 | — |
ar-EG | アラビア語(標準) | 現代標準アラビア語で話します。コードは、Google のリアルタイムモデルにある唯一のアラビア語のコードである ar-EG ですが、方言を指定するものではありません — 指示文で方言を求めたときだけ、その方言で話します |
- 韓国語・英語以外のボットは、運営者が指示文・あいさつを読んで許可したあとで電話を受けます — すべてのボットを受け付けるサーバーでも同じです。保存時の自動ルール検査(人のふり、なりすましなど)が、韓国語と英語しか読めないためです。許可の前は
unavailable_reason: "awaiting_review"で、許可のあとで指示文・あいさつ・ツールを変えると、再び許可を待ちます。@BotFather は、言語を選ぶ画面と選んだあとに "한국어·영어가 아닌 언어는 자동 규칙 검사가 읽지 못해서, 운영자가 지시문·인사말을 읽고 허용한 뒤에 전화를 받아요."(「韓国語・英語以外の言語は自動ルール検査で読めないため、運営者が指示文・あいさつを読んで許可したあとで電話を受けます。」)と知らせます。運営者の管理画面の音声タブには、そのボットが "한국어·영어가 아닌 봇 (자동 규칙 검사가 읽지 못함): 지시문·인사말을 읽어 주세요"(「韓国語・英語以外のボット(自動ルール検査では読めない):指示文・あいさつを読んでください」)と表示されます。 - ボットの言語で通話するゲートウェイがないと、電話を受けられません。ゲートウェイはサーバーにつながるときに、自分が通話できるボットの言語を伝えます(0.7.0 からは 9 言語すべて)。言語を伝えないゲートウェイは、韓国語と英語だけに対応するものとみなします。ボットの言語で通話するゲートウェイがなければ
no_gateway_with_languageになり、通話ボタンは表示されません。別の言語で代わりに通話することはありません。 - id は大文字・小文字を区別せずに受け付け(
ja-jp)、一覧の表記で保存します。一覧にない値は、受け付ける id を示すVOICE_LANGUAGE_INVALID: language is one of ko-KR, en-US, ja-JP, cmn-CN, fr-FR, es-US, id-ID, vi-VN, ar-EGで拒否されます。 - 選んだ言語があとで一覧から外れると、そのボットは設定を保ったまま電話を受けなくなります(
text_refused)。別の言語で代わりに受けることはないので、@BotFather の言語(언어)で選び直してください。 - クメール語はまだボットの言語として選べません。モデルは聞き取れますが、自由に話した返答をクメール語のネイティブスピーカーがまだ確認していないためです。
発信者がずっと別の言語で話すと、モデルはその言語で答えます。AI の案内音声は、ボットの language ではなく、発信者が見た確認画面の言語で流れます(発信者に見えるもの)。
通話の通知を受け取る — voice_call
ボットに電話がかかってくると、voice_call アップデートが届きます。音声のアップデート 3 種類(voice_call、voice_call_tool_query、voice_call_transcript)は、名前を書いて求めた場合にだけ届きます。getUpdates か setWebhook の allowed_updates に入れてください。リストが空でも届きません(ほかの種類は、空のリストならすべて届きます)。
curl -s "https://pabal.me/bot$BOT_TOKEN/getUpdates" -H 'Content-Type: application/json' \
-d '{"allowed_updates": ["message", "voice_call", "voice_call_tool_query"], "timeout": 30}'
{
"update_id": 7,
"voice_call": {
"id": "4611686018427387905",
"direction": "incoming",
"event": "started",
"from": {"id": 100002, "is_bot": false, "first_name": "ミンジ", "language_code": "ko"},
"date": 1790380803,
"max_duration": 180
}
}{
"update_id": 9,
"voice_call": {
"id": "4611686018427387905",
"direction": "incoming",
"event": "ended",
"from": {"id": 100002, "is_bot": false, "first_name": "ミンジ", "language_code": "ko"},
"date": 1790380952,
"start_date": 1790380803,
"duration": 149,
"max_duration": 180,
"end_reason": "caller"
}
}direction:ボットにかかってきた電話は"incoming"、ボットがかけた電話は"outgoing"です(ボットからの電話のアップデート)。以前のように、この項目がなければかかってきた電話と読んでも正しいままです。allowed_updatesはボットごとに 1 つの設定で、最後にgetUpdatesかsetWebhookが渡したものが残ります(渡さなければ前の値のまま)。そのため上の例のようにリストを渡すと、リストに書かなかった既定の種類(edited_message、callback_query)はもう届きません — ボットが使う種類はすべて書いてください。求める前に起きた通話の通知はためられないので、あとから求めても届きません。- 発信者(
from)は ID・名前・アプリの言語だけが届きます。ユーザー名と電話番号は届きません(確認画面でそう約束しています)。削除されたアカウントはfirst_name: "Deleted Account"。 - Webhook なら、ほかのアップデートと同じく順番に POST で届きます。getUpdates のキューはボットごとに直近 1,000 件までメモリにあり、サーバーを再起動すると消えます。
- 停止されたボット、削除されたボットには何も送られません。
event | いつ | 追加されるフィールド |
|---|---|---|
started | 発信者のアプリが通話を確認し(鍵交換の完了)、サーバーがゲートウェイに通話がつながったと伝えたとき。サーバーは音声を見ないので、サーバーから見た「つながった」です。Apple Watch の通話には鍵交換がないため、ゲートウェイがウォッチの音声がつながったと伝えたときです | max_duration |
ended | つながった通話が終わったとき。文字起こしを待つ場合は最大 10 秒遅れて届きます。会話を残す機能が有効なサーバーでは、ストリームモード以外のすべての通話が、発信者の選択に関係なく最大 10 秒遅れて届きます — 発信者の選択をボットのオーナーが知ることができないようにするためです | start_date、duration(サーバーの時計でつながっていた秒数)、max_duration、end_reason、(文字起こしが有効なら)transcript |
missed | つながる前に終わった通話(呼び出し中に切れた場合など) | end_reason |
refused | 発信者はすべての確認を通ったのに、ボット側の事情(同時通話、1 日の時間、ゲートウェイ)で受けられなかったときだけ(それ以外の拒否はボットのオーナーに知らせない) | refused_reason:busy(同時通話がいっぱい) · budget(ボット・オーナー・サーバーの 1 日の時間を使い切った) · unavailable(受けられるゲートウェイがいっぱい) |
end_reason:caller(発信者が切った) · bot(ボットが切った、endVoiceCall) · max_length(最大の長さ) · failed(接続の失敗) · operator(運営者・緊急停止スイッチ) · suspended(ボットの停止・削除)。
ゲートウェイに渡された通話ごとに、発信者とボットのチャットに通話記録(通話時間、または「キャンセルされた通話」)が残ります(受ける前に拒否された通話は残りません)。ボット側の写しは message アップデートとしては届きません — ボットの記録は voice_call です。発信者が会話を残すことを選ぶと、会話の文章は発信者のチャットにだけ通話記録のすぐ下に残り、ボット側には何も残りません。会話を残す機能が有効なサーバーでは、通話記録のあとのメッセージ番号を 30 個あらかじめ確保するため、通話記録の次のメッセージの番号は記録より 31 以上大きくなります(残さなかった通話も同じ)。
通話中に
切る — endVoiceCall
curl -s "https://pabal.me/bot$BOT_TOKEN/endVoiceCall" -d voice_call_id=4611686018427387905
farewell(Boolean、既定 true):つながっている通話なら、モデルが短く別れのあいさつをしてから切ります。10 秒以内に切れなければサーバーが終わらせます。falseならすぐに切ります。ストリームモードの通話は常にすぐ切ります。- 終わった通話の
voice_call.end_reasonはbotです。このボットの進行中の通話でなければ400 VOICE_CALL_NOT_FOUND。
モデルにメモを渡す — sendVoiceCallContext
通話中にモデルが知っておくべきことを伝えます。モデルはこれを発信者の言葉ではなくボットのオーナーの文章として読み、Pabal のルールが引き続き優先されます。
curl -s "https://pabal.me/bot$BOT_TOKEN/sendVoiceCallContext" -d voice_call_id=4611686018427387905 \
--data-urlencode "text=この発信者はたったいま A-1042 の注文をキャンセルしました。聞かれたらそう伝えてください。" -d respond=false
text:2,000 文字(6,144 バイト)まで、内容のルールで検査。respond=trueならモデルがすぐに反応し、既定の false なら次の番で使います。- メモもモデル(マネージド通話なら Google)に送られ、モデルが発信者に話すことがあります — 発信者に知られてもよいことだけを入れてください。
- つながる前なら
400 VOICE_CALL_NOT_ACTIVE、ストリーム通話やメモを受け付けないゲートウェイなら400 VOICE_CONTEXT_NOT_SUPPORTED、通話ごとに 1 分に 10 件を超えると429(retry_after60)。
使用量 — getVoiceCallUsage
curl -s "https://pabal.me/bot$BOT_TOKEN/getVoiceCallUsage?period=today"
{"ok": true, "result": {"period": "today", "from_date": 1790348400, "to_date": 1790434799,
"calls": 3, "seconds": 412, "missed": 1,
"refused": {"busy": 0, "budget": 0, "unavailable": 0},
"platform_seconds": 412, "own_key_seconds": 0,
"remaining_seconds_today": {"bot": 3188, "owner": 6788}}}
period:today(既定)またはmonth。monthならmonth=2026-09のように月を選べます(未来の月はVOICE_USAGE_PERIOD_INVALID)。- 日と月の区切りはサーバーのタイムゾーン(既定
Asia/Seoul)です。remaining_seconds_todayは、1 日の上限から終わった通話といま進行中の通話が確保している時間を引いた値です。 - 通話ごとの使用記録(発信者は記録しない)は、その月が終わってから 180 日間保管し、月ごとの合計はボットがある限り残ります。
ツール — 通話中にボットのコードを呼ぶ
発信者が「注文 A-1042 はいまどこですか?」と尋ねると、モデルがボットの check_order ツールを呼び、サーバーがその問い合わせをボットに渡し、ボットの答えをモデルが言葉で伝えます。ツールはボットのオーナーが定義し、ボットのコードが答えます。
図の説明
- 3 つの列:左がモデルと音声ゲートウェイ、中央が Pabal サーバー、右がボットのオーナーのコードです。縦の点線が時間です。モデルはボットと直接話さず、すべての問い合わせと答えがサーバーを通ります。
- ① の灰色の枠 — サーバーがまず引数を検査します:ツールの
parametersに合っているか、大きさと深さ、宣言されていないキー(取り除いて渡す)、カード番号や住民登録番号のように見える値(ボットには届かず、モデルが拒否を聞く)。 - ② でボットが受け取るものは、問い合わせ id、通話 id、発信者、ツール名、引数(JSON 文字列)、そして
budget_ms— 答えがサーバーに届くべき期限までの残りミリ秒です。待っているあいだは、ゲートウェイが沈黙を埋めます(左の枠)。 - ③ の答えも検査します:秘密のように見えるもの、ルールを破らせる言葉、カード番号・住民登録番号は拒否され、モデルはエラーを聞きます。最初の答えが最後なので、拒否された答えでも問い合わせは閉じられます。
- ④ の青い実線が正常、赤い点線が期限を過ぎた場合です。モデルはツールの結果を指示ではなくデータとしてのみ受け取り、失敗なら Pabal の短い文言を聞いて、発信者に事情を話します。
ツールを決める
curl -s "https://pabal.me/bot$BOT_TOKEN/setVoiceCallSettings" -H 'Content-Type: application/json' -d '{
"instructions": "あなたは注文ヘルパー店の電話窓口です。丁寧に短く答えてください。発信者が注文番号を言ったら、check_order ツールで配送状況を調べて伝えてください。",
"tools": [{
"name": "check_order",
"description": "注文番号で配送状況を調べる。発信者が注文番号を言ったら呼ぶ。",
"parameters": {"type": "object",
"properties": {"order_id": {"type": "string", "description": "注文番号、例: A-1042"}},
"required": ["order_id"]},
"timeout_ms": 5000
}]
}'
| 項目 | ルール |
|---|---|
| キー | name、description、parameters(任意)、timeout_ms(任意)の 4 つだけ |
| 数・大きさ | ボットのツールと固定された MCP ツールを合わせて 16 個、定義(name・description・parameters)の UTF-8 の合計が 8,192 バイト。自前のキーのボットも同じです |
name | [A-Za-z_][A-Za-z0-9_-]{0,63}。__ を含められず(MCP ツールの名前に使う)、pabal で始められず、大文字・小文字を区別せずに重複できません |
description | 必須、1,024 バイトまで、内容のルールで検査。モデルはこれを見ていつ呼ぶかを決めるので、具体的に |
parameters | 最上位は {"type": "object"}。省くと「引数なし」 |
timeout_ms | 1,000〜15,000、既定 5,000。モデルが答えを待つ最大の時間 |
parameters には、2 つのエンジンがどちらも受け付ける JSON Schema の一部だけを使います。
- キーワードは
type、description、enum、properties、required、items、minimum、maximum、minItems、maxItemsの 10 個だけ。typeは 1 つ(string・number・integer・boolean・array・object)。 properties・requiredは object に、items・minItems・maxItemsは array に(itemsは必須)、minimum・maximumは数に、enumは string にだけ(1〜32 個、それぞれ 64 バイト)。- プロパティ名は
[A-Za-z_][A-Za-z0-9_]{0,63}、1 つのツールにプロパティ 24 個、object は 3 段・object と array を合わせて 5 段まで。パラメーターの説明は 512 バイト。 - 秘密を尋ねるパラメーターは拒否します:名前に
pin・cvv・password・otp・ssn・rrn・cardnumberのような語があるか、説明がカード番号・パスワード・認証番号・住民登録番号に触れているとasks_secrets。 - エラーは最初の問題 1 つをパス付きで知らせます:
VOICE_TOOL_INVALID: tools[0].parameters.properties.tags: items_missing、VOICE_TOOL_INVALID: tools[0].name: reserved、VOICE_CONTENT_REFUSED: tools[0].description: impersonation。 - ツールは Bot API でだけ決めます。@BotFather の音声通話の画面は、ツールの名前を表示するだけです。
- 許可したボットだけを受け付けるサーバーでは、ツールを追加したり変えたりすると、運営者が再び許可するまで電話を受けません(
awaiting_review、有効にするの手順 4)。
ツール問い合わせを受け取る
{
"update_id": 813,
"voice_call_tool_query": {
"id": "8190448532776044545",
"voice_call_id": "4611686018427387905",
"from": {"id": 100002, "is_bot": false, "first_name": "ミンジ", "language_code": "ko"},
"name": "check_order",
"arguments": "{\"order_id\":\"A-1042\"}",
"budget_ms": 4700
}
}
argumentsは JSON オブジェクトのテキストです(4,096 バイト・深さ 8 以下、サーバーが検査済み)。それでも発信者の言葉からモデルが作った値なので、ボットでもう一度確かめてください。注文番号さえわかれば他人の注文を聞けてしまう、ということがあってはいけません —from.id(Pabal が確認した発信者)のものだけを伝えてください。budget_msは、ボットが受け取った時点から答えがサーバーに届くべき期限までのミリ秒です。ツールのtimeout_msから、モデルまで戻る道のり(200 ms)とボットまで来る道のり(100 ms)を引いた値なので、ふつうはtimeout_msより約 300 ms 少なくなります。お互いの時計は使いません。
問い合わせがボットに届く経路は、次の順で決まります(getVoiceCallSettings.tool_delivery)。
- Webhook があれば Webhook で。Webhook の
allowed_updatesにvoice_call_tool_queryが必要で、なければほかの経路には回らず届きません(tool_delivery_problem: "not_in_allowed_updates")。問い合わせは別に送る POST なので、順番待ちのキューにふさがれることはなく、再送もしません。 - Webhook がなく、直近 15 秒以内に getUpdates を呼んでいれば、次の getUpdates の先頭に。こちらも
allowed_updatesに入っている必要があります。1 つのボットで待機できる問い合わせは 8 件まで。 - それ以外で、MTProto で接続しているボットなら
updateBotWebhookJSONQueryで(allowed_updatesに関係なく)。 - どれにも当たらなければ届かず(
tool_delivery_problem: "not_listening")、モデルは「The tool could not be reached.」(ツールに接続できませんでした)を聞きます。
答える — answerVoiceCallToolQuery
| パラメーター | 説明 |
|---|---|
tool_query_id | アップデートの voice_call_tool_query.id |
result | 結果。テキスト、または JSON 本文ならオブジェクト・配列(テキストで送っても JSON のオブジェクト・配列として読めれば JSON として扱う)。UTF-8 で 16,384 バイトまで(運営者がさらに下げることがある) |
error | ボット自身の失敗の文言、512 バイトまで。モデルはこの言葉を発信者に伝えます |
resultとerrorのちょうど一方(どちらもなければTOOL_RESULT_EMPTY、両方ならTOOL_RESULT_INVALID)。戻り値は True。- ツールの答えはモデル(マネージド通話なら Google)に送られ、モデルがその内容を発信者に話します — 発信者に知られてもよいことだけを入れてください。
- 最初の答えが最後です。遅れた、または終わった問い合わせには
400 TOOL_QUERY_EXPIRED: <理由>—answered(すでに回答済み)、timeout、cancelled(発信者が割り込んでモデルが取り下げた)、call_ended、unreachable。知らない id や 10 分を過ぎた id はTOOL_QUERY_ID_INVALID。 - 答えも検査します:見えない文字は取り除き、秘密のように見えるもの(
looks_like_secret)、ルールを破らせる言葉(overrides_rules・hides_ai・claims_human・asks_secrets)、カード番号・住民登録番号(sensitive_data)はTOOL_RESULT_REFUSED: <コード>。店の住所・電話番号・URL のようなボットのオーナーのデータは通ります。大きさを超えるとTOOL_RESULT_TOO_LONG・TOOL_ERROR_TOO_LONG。 - Webhook ならレスポンス本文で答えられます — ツール問い合わせの POST に対する 200 レスポンスにこれを入れるだけです(ほかのメソッドは実行しません)。答える前に
X-Telegram-Bot-Api-Secret-Tokenヘッダーを必ず確認してください。確認せずに顧客のデータをレスポンスに入れると、そのアドレスに偽の問い合わせを送る誰もがその答えを受け取れてしまいます。
{"method": "answerVoiceCallToolQuery", "tool_query_id": "8190448532776044545",
"result": {"order_id": "A-1042", "status": "配送中", "eta": "明日の午後に到着"}}
例 — python-telegram-bot
上の check_order ツールに答えるボットです。getUpdates で受け取り、問い合わせをほかのアップデートと同時に処理します(concurrent_updates)— 前のアップデートの処理で遅れると budget_ms が過ぎてしまいます。注文は注文した人の Pabal ユーザー ID で探し、問い合わせの from.id と一致したときだけ伝えます。
# order_voice_bot.py — pip install "python-telegram-bot==22.*"
# 実行: read -rs BOT_TOKEN && export BOT_TOKEN
# CUSTOMER_ID=<注文した人の Pabal ユーザー ID> python3 order_voice_bot.py
# 試すときは CUSTOMER_ID に自分の ID を — ボットにメッセージを送ったあとの getUpdates の message.from.id です
import json
import os
import re
from telegram import Update
from telegram.error import TelegramError
from telegram.ext import Application, ContextTypes, TypeHandler
SERVER = "https://pabal.me"
CUSTOMER = int(os.environ.get("CUSTOMER_ID", "0"))
ORDERS = { # 例のための小さな表: 実際には店の注文システム
(CUSTOMER, "A-1042"): {"status": "配送中", "eta": "明日の午後に到着"},
}
def check_order(arguments, caller_id):
"""answerVoiceCallToolQuery にそのまま渡す {"result": …} または {"error": …}"""
order_id = str(json.loads(arguments).get("order_id", "")).strip().upper()
if not re.fullmatch(r"[A-Z]-\d{4}", order_id): # 引数はモデルが作ったもの — もう一度確認
return {"error": "注文番号を聞き取れませんでした。例: A-1042"}
found = ORDERS.get((caller_id, order_id)) # 電話をかけた人の注文だけ
if found is None: # 他人の注文も、ない注文と同じ言葉で
return {"error": f"{order_id} の注文が見つかりませんでした。"}
return {"result": json.dumps({"order_id": order_id, **found}, ensure_ascii=False)}
async def on_update(update: Update, context: ContextTypes.DEFAULT_TYPE):
q = update.api_kwargs.get("voice_call_tool_query") # PTB が知らない種類は api_kwargs に入る
if q is None:
return
if q["name"] == "check_order":
answer = check_order(q["arguments"], q["from"]["id"])
else:
answer = {"error": "知らないツールです。"}
try:
await context.bot.do_api_request("answerVoiceCallToolQuery",
api_kwargs={"tool_query_id": q["id"], **answer})
except TelegramError as e: # TOOL_QUERY_EXPIRED: 遅すぎたか、通話が終わった
print("回答が受け付けられませんでした:", e, flush=True)
app = (Application.builder().token(os.environ["BOT_TOKEN"])
.base_url(f"{SERVER}/bot").base_file_url(f"{SERVER}/file/bot")
.concurrent_updates(True).build())
app.add_handler(TypeHandler(Update, on_update))
app.run_polling(allowed_updates=["message", "voice_call", "voice_call_tool_query"], poll_interval=0, timeout=10)
aiogram は知らないアップデートの種類を読み飛ばすので、ディスパッチャーの外側のミドルウェア(dp.update.outer_middleware)で、update.model_extra の voice_call_tool_query を先に受け取る必要があります。Pabal のリポジトリに例(scripts/bots/voice_tools_aiogram.py)があるので、必要ならサーバーの運営者に頼んでください。
モデルと発信者が聞くもの
- 成功:結果がモデルの関数の応答になります(JSON なら JSON の値として)。結果の中の文章はデータとしてのみ読まれ、指示にはなりません。
- 失敗:モデルは通話の言語で Pabal の短い文言を聞き、発信者に事情を話します — 期限切れなら「The tool did not answer in time.」(ツールが時間内に答えませんでした)、届かなければ「The tool could not be reached.」(ツールに接続できませんでした)、カード番号のような引数なら「Pabal does not pass on card numbers or national ID numbers.」(Pabal はカード番号や住民登録番号のような情報を渡しません)、ボットの
errorはその文言のまま。誤った引数にはどこが誤っているかのヒントが送られるので、モデルは呼び直せます。 - 待っているあいだは、ゲートウェイが沈黙を埋めます:1.5 秒静かなら「One moment, let me check.」(少々お待ちください、確認します)を 1 回(最近応答が遅いツールならすぐに)、5 秒を過ぎると「Just a moment longer, please.」(もう少しお待ちください)を 1 回。この 2 つの言葉は発信者の言語ではなくボットの言語で流れ、その言語の確認済みの録音がなければ英語で流れます(いま確認済みなのは韓国語と英語)。
- 発信者が話をさえぎって入ってくると(割り込み)、ゲートウェイは開いている問い合わせを取り下げることがあり、そのあとの答えは
TOOL_QUERY_EXPIRED: cancelledになります。
ツールの上限
同時に開いている問い合わせは通話ごとに 2 件、ボットごとに 8 件。呼び出し回数は通話ごとに 1 分に 30 回、ボットごとに 1 分に 120 回。超えるとボットには送られず、モデルがすぐに too_many_open・rate_limited を聞きます。
ボットオーナー規約
文字起こし、自前のモデルキー、ストリームモード、MCP サーバー、ボットからの電話は、ボットオーナー規約(第 4 版)に同意しないと有効にできません。同意は @BotFather の音声通話の画面(文字起こし・自前のキー・ストリームモード・MCP サーバー・ボットからの電話のどの画面からでも)で人が行います — トークンではできません。前の版に同意していても、もう一度尋ねられます。サーバーの利用規約にボットオーナーの節が掲載されるまでは、同意のボタンは表示されません(その前に呼ぶと 403 VOICE_OWNER_TERMS_NOT_ACCEPTED)。
BotFather は規約を韓国語で表示します。ボタンは「規約に同意する」という意味です。要点は次のとおりです。1. 文字起こしを有効にすると、通話が終わったときに AI が書き取った会話の文章を受け取り、その扱いと保護は自分の責任になる。2. 文字起こしが有効なボットに電話した人には、通話がつながったときに、文章がボットのオーナーに送られることを案内音声でまず知らせる。3. 受け取った文章は必要な分だけ保管し、発信者に消してほしいと言われたら消し、モデルの学習には使わない。4. 自前のモデルキーを使うと、通話の音声と文章は自分のキーの提供元に自分の契約で送られ、その処理の責任は自分にあり、自分のプライバシー案内にそう書く。5. ストリームモードでは、電話の音声は Pabal の AI モデルではなく自分のサーバーに送られ、それを受け取って処理するのは自分であり、発信者の声と話した内容は法律が許す範囲でだけ使い、不要になったら消す。6. 自分のサーバーがどこにあり何をするのかを発信者に聞かれたら答える(ボットの説明に書いておくとよい)。7. ボットは常に AI であると名乗り、返答は AI が作る — 人が直接答えたり、人の録音を AI の返答のように流したりしない。ストリームモードでは、Pabal が通話の冒頭で「This is an AI voice bot. The call's audio goes to the bot developer's server.」(AI 音声ボットです。通話の音声はボット開発者のサーバーに送られます)という案内を流す。8. カード番号・住民登録番号・パスワード・認証番号を尋ねず、音声ボットのルールも守る。9. Pabal は文章を保存しない — 届けたら遅くとも 24 時間後にはサーバーから消え、キーやストリームモード・MCP サーバーのシークレットは暗号化して保管し、どこにも記録しない。10. MCP サーバーを登録すると、通話中にモデルがそのサーバーのツールを呼ぶとき、発信者が話した値がそのサーバーに送られ、そのサーバーとその先の処理の責任は自分にあり、送り先は事実どおりに申告する。11. 規約に違反すると、運営者が音声通話やボットを停止できる。
12–24 項はボットからの電話を有効にするときに守ることです:宣言して運営者が許可した目的にだけ使い、広告をしない。最初のひと言でボットの名前と AI であることを伝える。context に機微な情報を入れない。19 歳未満の人にかけようとしない。着信しない時間帯・一時停止・上限を避けようとしない。着信者の情報は目的にだけ使い、許可がオフになったら消す。トークンを守る。ボットがかけた電話には文字起こしは届きません(24 項)。規約の文言は法的な確認のあとで変わることがあります。
MCP サーバー
運営者が MCP の部品(pabal-mcp、署名器、送信プロキシ)をそろえ、プライバシーポリシーの bot-calls-mcp 節と利用規約のボットオーナー節を掲載すると有効になります(MCP のリクエストが出ていく専用の送信元アドレスを別に用意するのは運用上の条件で、コードは確認しません)。有効になったあとも、アプリが確認画面にボットの MCP の送り先を表示するようになるまでは、登録だけされ、どの通話にも MCP ツールは載りません(mcp_unavailable_reason: "sheet")。MCP が無効なサーバーでは setVoiceCallMcpServer が 403 VOICE_NOT_AVAILABLE: MCP tools are not available on this server を返し、@BotFather に「MCP サーバー」(MCP 서버)ボタンはありません。
ボットのオーナーが運営する(または契約した)MCP サーバーを登録すると、そのサーバーのツールをモデルが通話中に呼べます。呼ぶのはボットのコードではなく Pabal です — Pabal の MCP クライアントが Pabal の送信プロキシを通してそのサーバーにリクエストし、期限・大きさ・検査を守ります。そのため、発信者が話した値がそのサーバーに送られます(ボットオーナー規約 10 番)。
MCP サーバーに必要なもの
- Streamable HTTP、POST(プロトコルのバージョン 2024-11-05 以上)。
https://で公的な証明書、リダイレクトなし、ポートはサーバーが許可するものだけ(pabal.me は 443)。 - 資格情報を使うサーバーは、資格情報がなければ正直に
401で拒否し、誤った資格情報を受け入れてはいけません。1 つの資格情報は 1 つのボットしか使えません。 - 運営者ドキュメント:サーバーの運営者は
https://<ホスト>/.well-known/pabal-voice-mcpに JSON ドキュメント(32KB 以下、リダイレクトなし)を置きます — 運営主体・国・連絡先、目的、データの種類と所在地、送り先と再委託先、保管期間。発信者の確認画面に表示される送り先の情報で、なければボットのオーナーが登録するときに申告します(下記)。資格情報なしで誰でも呼べる公開サーバー(auth.type: "none")は、このドキュメントのエンドポイントの項目に"accepts": "any"と書いて、Pabal からの呼び出しを受け入れると明示する必要があります。 - Pabal が付けるもの:
tools/callの_metaに、残りの期限me.pabal/budget_ms(ミリ秒)と結果の大きさの上限me.pabal/result_budget_bytesが付き、Pabal が署名したアサーション(me.pabal/assertion、JWT)が付くことがあります。アサーションの発行者・sub(bot:<ボット id>)・検証鍵(JWKS)はgetVoiceCallMcpBindingでわかります。 tools/listは 2MB・20 ページ以下。人が介在しなければならない手順(ブラウザーでのログインなど)があってはいけません。- ツールごとに
_meta["me.pabal/voice"]で分類を与えます —data(none・caller・third_party)、content(system・third_party_text)、effect(none・write・notify・financial)、max_ms(15,000 以下)、max_result_bytes(65,536 以下)、任意のp95_ms。サーバーが与えなければ、ボットのオーナーが登録するときにtoolsで上書きします。 - 規格 1.0 は L1 だけを与えます:
data: "none"かつeffect: "none"のツール(公開された情報を読むだけのツール)だけが通話に固定されます。発信者のデータを扱う(L2)、または何かを実行する(L3)ツールは記録だけされ、使われません。 - 登録するとき、Pabal が適合性テストを実行します(アドレス・TLS・バージョン・401・ツールの一覧・ツールの構造・資格情報の範囲・人の介在)。不合格なら
MCP_CONFORMANCE_FAILED: CT-xxで拒否されます。
登録する
シークレットはリクエスト本文でのみ送ります(クエリ文字列なら 400)。コマンドラインにシークレットが残らないよう、ファイルに置いて送ってください。
umask 077
cat > mcp-shop.json <<'EOF'
{"label": "shop",
"url": "https://mcp.example.com/mcp",
"auth": {"type": "bearer", "token": "(MCP サーバーから受け取ったトークン)"}}
EOF
curl -s "https://pabal.me/bot$BOT_TOKEN/setVoiceCallMcpServer" -H 'Content-Type: application/json' -d @mcp-shop.json
label:[a-z][a-z0-9]{0,11}、pabalで始められない。ボットごとにサーバー 2 つ。モデルから見えるツールの名前はlabel__ツール名です。auth.type:none、bearer(token)、headers(ヘッダー 3 つまで)、client_credentials(token_url、client_id、client_secret、またはprivate_key_jwt: true— 先にcreateVoiceCallMcpClientKey)。authorization_codeはまだありません。none(公開サーバー)は、そのサーバーの運営者の公開ドキュメントが Pabal からの呼び出しを受け入れると明示している場合にだけ登録されます(そうでなければMCP_OPT_IN_REQUIRED)。- 送り先が変わる変更(新しいサーバー、別のアドレス・トークンのホスト、新しい申告、より高い等級)は、リクエストの答えが
400 MCP_CHANGE_NEEDS_CONFIRMATIONになり、@BotFather に表示される確認(확인 — 이 변경을 적용)ボタンをボットのオーナーが 5 分以内に押すと適用されます。トークンが漏れても、発信者の言葉がひそかに別の場所へ送られないようにするためです。シークレットだけを変えるローテーションはすぐに反映されます。 - サーバーの運営者の公開ドキュメントがなければ、ボットのオーナーが
declarationで送り先(運営主体・国・連絡先・目的・データの種類・所在地・再委託先・保管期間)を事実どおりに申告します。発信者の確認画面にこれが表示され、変わると発信者にもう一度尋ねます。 - 登録・検査はボットごとに 1 時間 10 回、オーナーごとに 20 回。ほかのメソッド:
refreshVoiceCallMcpServer(再検査)、deleteVoiceCallMcpServer(いつでもできる)、getVoiceCallMcpBinding(自分のサーバーが Pabal からの呼び出しを見分けるのに使う発行者・sub・JWKS)、createVoiceCallMcpClientKey。 - 登録したサーバーとツールの状態は
getVoiceCallSettings.mcp_serversに表示されます(パス・クエリ・シークレットは表示されない)。規格の全文(Pabal Voice MCP Profile 1.0)は Pabal のリポジトリのdocs/app/voice-bot-mcp-profile.htmlにあるので、必要ならサーバーの運営者に頼んでください。
自前のモデルキー
自前のキーで受ける通話では、発信者がそのキーの提供元に音声が送られることを示す確認画面(第 2 版)を見る必要がありますが、その画面を表示するアプリはまだありません。そのため、テストサーバー以外のサーバーは、アプリの準備ができたとして運営者が有効にするまで、キーを受け付けもしません(403 VOICE_OWN_KEY_NOT_AVAILABLE)。サーバーにマスターキーも必要です。
ボットの通話を、ボットのオーナー自身のモデルのアカウントで動かします。料金はオーナーの提供元のアカウントに請求され、自前のキーの通話はプラットフォームのボット・オーナー・サーバーの 1 日の上限を使いません(発信者の上限はそのまま)。ボットオーナー規約が先に必要です。
curl -s "https://pabal.me/bot$BOT_TOKEN/setVoiceCallApiKey" -d provider=google \
--data-urlencode "api_key@vertex-sa.json"
provider:googleは Vertex AI のサービス アカウントの JSON キー(必要な項目だけを保管)、openaiはsk-で始まる API キー。それ以外の値はVOICE_PROVIDER_INVALID。キーはリクエスト本文でのみ(クエリなら 400)— 上のようにファイルから読んで送れば、コマンドラインに残りません。@BotFather の会話に貼り付けないでください。- サーバーはキーを受け付ける前に、ゲートウェイのテストセッションで実際に開けるかを確かめます(最大 12 秒)。通れば保管して True。提供元が拒否すれば
API_KEY_INVALID、ログインはできるがリアルタイム API の権限がなければAPI_KEY_NO_REALTIME_ACCESS、テストできるゲートウェイがないか混んでいれば503 API_KEY_CHECK_UNAVAILABLE: …: the key was not kept。 - テストはボットごとに 1 時間 5 回、オーナーごと(すべてのボットの合計)に 10 回。ゲートウェイに届いたテストだけを数えます。
- キーはどこにも表示されません — 答え・エラー・ログ・画面のすべてで。
getVoiceCallSettings.api_keyには提供元・設定日・フィンガープリント(8 文字)・使えるかどうかだけがあります。 - 設定したキーが使えないと(規約が変わった、サーバーがキーを開けないなど)、ボットは電話を受けません — プラットフォームのキーにひそかに切り替えることはしません。
deleteVoiceCallApiKey(いつでもできる)でキーを削除するとプラットフォームのキーで受けるようになり、そのキーで進行中だった通話は終わります。 - 自前のキーの通話には Pabal の 1 日の上限がありません — 1 回の通話の長さ、同時通話、発信者の上限だけがかかります。料金は自分のアカウントに請求されるので、提供元のコンソールで予算アラートを設定しておいてください。
- OpenAI のキーは、ゲートウェイの運営者が別に有効にしないと使われません(ゲートウェイの既定は Google のキーのみ)。有効にしたゲートウェイがなければ、OpenAI のキーのテストは
503 … no_gatewayです。OpenAI のキーの経路は、まだ実際の OpenAI ではテストしておらず(偽のサーバーでのみ)、OpenAI のキーで受ける通話は、ボットの声の設定に関係なく、ゲートウェイが決めた OpenAI の声(既定marin)を使います。Google のキーは、ゲートウェイの Vertex のリージョン(us-central1)で使われます。 - ストリームモードでは、キーは保管するだけで使いません。
文字起こし
運営者が法的な確認を終えて、管理画面で許可する必要があります(既定は無効)。許可されていないサーバーでスイッチをオンにしても、transcript_available: false、transcript_unavailable_reason: "not_allowed" となり、文章は届きません。
有効にすると、AI が書き取った通話の会話の文章をボットのオーナーが受け取ります。ボットオーナー規約に同意してから、スイッチをオンにします。
文字起こし(この節)はボットのオーナーがオンにしてボットのオーナーが受け取り、発信者はつながったときに案内音声を聞きます。会話を残すは電話をかける人が確認画面のチェックボックスで選び、通話が終わったあとその人自身のチャットにだけ文字で残ります — ボットにもボットのオーナーにも送られず、案内音声もありません。1 つの通話が両方を持つこともあります。会話を残す機能にはボットのオーナーがオンにするスイッチはなく、サーバーのプライバシーポリシーに bot-calls-keep-text 節があれば有効になります(準備中 — アプリの使い方 — 音声ボット)。
curl -s "https://pabal.me/bot$BOT_TOKEN/setVoiceCallSettings" -d transcript=true
- ボットがかけた電話には文字起こしは届きません(ボットからの電話)— スイッチをオンにしていても同じです。
- 文字起こしが有効なボットに電話すると、発信者は AI の案内のすぐあとに「What is said on this call is written down and sent to the bot's owner.」(この通話の会話の内容は文字にされ、ボットのオーナーに送られます)を聞きます。案内が終わったあとの言葉だけを書き取ります。ストリームモードの通話には文字起こしはありません。
- 終わった通話の
voice_call(event: "ended")にtranscriptが入って届きます。これが唯一の完全版なので、allowed_updatesにvoice_callを入れておいてください。通話中に 1 発話ずつ受け取りたければvoice_call_transcriptも入れます(HTTP Bot API でのみ。MTProto では届きません)。 - カード番号や住民登録番号を含む発話は、すべての数字を
*に置き換えて届けます。2 つの発話にまたがって言った数字や、言葉で読み上げた数字は見つけられないので、受け取った文章も注意して扱ってください。 - 1 発話 4,096 バイト、1 通話 256KiB・4,000 発話まで。超えると残りは捨てられ、
truncated: true。 - Pabal は文章を保存しません:通話のあいだメモリにあり、ボットに 1 回渡し、受け取られなかったものは 24 時間後に消します。サーバーを再起動すると消えます。受け取ったあとの保管と処理は、ボットのオーナーの責任です。
"transcript": {
"turns": [
{"role": "bot", "text": "注文ヘルパーです。ご用件をどうぞ。", "offset_ms": 5200},
{"role": "caller", "text": "注文 A-1042 はいまどこですか?", "offset_ms": 9800}
],
"truncated": false
}{"update_id": 21, "voice_call_transcript": {"voice_call_id": "4611686018427387905", "seq": 2,
"role": "caller", "text": "注文 A-1042 はいまどこですか?", "is_final": true, "offset_ms": 9800, "date": 1790380813}}role は caller(発信者)または bot(モデル)、offset_ms は通話の音声が始まったときからのミリ秒です。
ストリームモード
発信者のアプリが「通話の音声はボット開発者のサーバーに送られる」というストリーム用の確認画面を表示しないとストリームモードのボットに電話できませんが、その画面を含むアプリのビルドはまだありません。サーバーにも、マスターキー、プライバシーポリシーの bot-calls-stream 節、ボットオーナーの節、ストリームを受け付けるゲートウェイが必要です。なければ mode=stream は 403 VOICE_STREAM_NOT_AVAILABLE です。
ストリームモードでは、電話の生の音声が Pabal の AI モデルではなくボットのオーナーのサーバーに送られます。自分のサーバーが音声を聞き、返答を音声で送り返します — どの AI を使うかは自分で決めます(別の提供元のリアルタイムモデル、自前の音声認識と音声合成の組み合わせなど)。Pabal は引き続き冒頭で AI の案内音声を流し、発信者の確認・上限・通話の長さ・同時通話の上限を守ります。指示文は不要で、ツール・自前のキー・文字起こしは保管するだけで使いません(stream_ignores)。
図の説明
- 3 つの列:左が発信者、中央が Pabal(サーバーと音声ゲートウェイ)、右がボットのオーナーのサーバーです。ボットのサーバーはアプリとも Pabal サーバーとも直接話さず、ゲートウェイとだけ WebSocket でやり取りします。
- ②〜④ は呼び出し中に起こります:ゲートウェイが確認した公的なアドレスに TLS で接続し、署名ヘッダーを付けてアップグレードし、
startを送ります。ボットのサーバーが 3 秒以内にreadyを送れば電話を受け、そうでなければ発信者には話し中音が聞こえます。 - 灰色の枠が関門です:AI の案内音声(「This is an AI voice bot. The call's audio goes to the bot developer's server.」)が流れ終わるまでは、発信者の音声はボットのサーバーに送られず、ボットが先に送った音声は再生されません。終わった瞬間に ⑤
liveが送られます。 - ⑥ 音声は両方向とも PCM16・24 kHz・モノラルで、発信者側は常に 20 ms(960 バイト)単位です。青い矢印は Pabal が送るもの、灰色はボットのサーバーが送るものです。
- ⑦ と ⑧:ボットは割り込み(
clear)、再生位置の通知(mark)、終話(hangup)を送れ、通話が終わると Pabal が理由を入れたstopを送って 1000 で閉じます。
設定とプローブ
まず、下の例のサーバーを公的なアドレス(証明書が合う名前)で起動しておいてください。プローブは設定した瞬間にそのサーバーへ接続し、届かなかった失敗も 1 時間 5 回の上限に数えます。
ボットオーナー規約に同意したあと(@BotFather → 音声通話 → ストリームモード → ボットオーナー規約に同意(봇 주인 약관에 동의))、サーバーのアドレスとシークレットを伝えます。シークレットは 32〜128 文字の表示可能な ASCII(空白なし)で、新しいアドレスは常に新しいシークレットと一緒に送ります。
umask 077
openssl rand -hex 32 | tr -d '\n' > stream-secret.txt # 末尾に改行があってはいけません
curl -s "https://pabal.me/bot$BOT_TOKEN/setVoiceCallSettings" -d mode=stream \
-d stream_url=wss://bot.example.com:8443/voice --data-urlencode "stream_secret@stream-secret.txt"
- アドレスの形式:
wss://のみ、ユーザー名・#・空白なし、ASCII のみ(ほかの文字はパーセントエンコード)、2,048 文字以下。ホストはドットを含む名前か公的な IP(localhostは不可)、ポートは 443 または 1024〜65535。そうでなければ400 STREAM_URL_INVALID: …。 - プローブの上限はリクエストした瞬間に数えます:ボットごとに 1 時間 5 回、オーナーごとに 10 回、同時にはボットごとに 1 つ・オーナーごとに 2 つ。超えると
429。ゲートウェイが混んでいてどこにも接続しなかったプローブは数えません。 - アドレスの確認:名前がプライベート・予約済みのアドレスや Pabal 自身のアドレスを 1 つでも指していれば、
400 STREAM_URL_INVALID: the host must resolve only to public addresses。 - プローブ:ゲートウェイが 1 回接続してみます(
startに"probe": true)。readyが来れば有効になり、シークレットはサーバーのマスターキーの下で暗号化して保管します。失敗すると400 STREAM_PROBE_FAILED: <コード>—unreachable(名前・接続・時間・TLS のいずれか)、handshake_failed(101 以外の応答 — たいていは署名が拒否された)、redirect、no_subprotocol、no_ready、bad_ready、address_refused、busy、urlなど — それ以外のコードはゲートウェイが返したまま届きます。サーバーは 12 秒まで待ちます。 - Bot API(トークン)でストリームモードが有効になったりアドレスが変わったりすると、@BotFather がボットのオーナーに「ストリームのアドレスが … に変わりました(シークレット …)」(원음 주소가 … 로 바뀌었어요 (비밀 …))とマネージドに戻す(관리형으로 돌아가기)ボタンを、マネージドに戻るとそのことと音声通話(음성 통화)ボタンを送ります。トークンが漏れて音声がひそかに別のサーバーへ送られるのを防ぐためです。
stream_secretをクエリ文字列に入れると、何よりも先に 400 になります。Webhook のレスポンスではストリームモードを設定できません。- マネージドに戻すには
mode=managed(アドレスとシークレットを削除)。有効なまま指示文が空ならVOICE_INSTRUCTIONS_REQUIREDになるので、指示文も一緒に送ってください。 - 運営者が許可したボットだけを受け付けるサーバーでは、運営者がストリームのアドレスを確認して再び許可すると電話を受けます。
getVoiceCallSettingsにはstream_urlとシークレットのフィンガープリントだけが表示されます。ストリーム通話ではsendVoiceCallContextは使えず(VOICE_CONTEXT_NOT_SUPPORTED)、endVoiceCallは別れのあいさつなしにすぐ切ります。
仕様
- TLS 1.2 以上、証明書は URL の名前に合う公的な証明書。ゲートウェイは確認した IP に接続しますが、SNI と証明書の検査は名前で行います。リダイレクト(3xx)には従いません。
- サブプロトコル
pabal.voice.v1を選ぶ必要があります(Sec-WebSocket-Protocol)。圧縮なし。1 メッセージ最大 65,536 バイト、テキストメッセージ最大 4,096 バイト(UTF-8)。 - ゲートウェイは 15 秒ごとに ping を送ります。15 秒以内に pong がなければ切断します(たいていのライブラリは自動で答えます)。
- テキストメッセージは JSON のイベント、バイナリメッセージは音声です。音声の形式は 1 つだけ:PCM16 リトルエンディアン、モノラル、24,000 Hz。
call_id・bot_idは 10 進数の文字列です。
| 方向 | メッセージ | いつ · 意味 |
|---|---|---|
| Pabal → ボット | {"event":"start","protocol":1,"call_id":"…","bot_id":"…","probe":false,"caller":{"lang":"ko"},"format":{"encoding":"pcm_s16le","sample_rate":24000,"channels":1,"frame_ms":20},"max_seconds":300} | アップグレードの直後に 1 回。発信者の名前・番号・ID はありません(caller.lang だけ — 人の情報は voice_call アップデートで) |
| Pabal → ボット | {"event":"live"} | AI の案内が終わった。ここから音声が行き来します |
| Pabal → ボット | バイナリ | 発信者の音声、常に 960 バイト(20 ms)。live のあとだけ |
| Pabal → ボット | {"event":"mark","name":"…"} | ボットが送ったその mark より前の音声がすべて再生された |
| Pabal → ボット | {"event":"cleared","played_ms":N,"dropped_ms":N,"marks":["…"]} | clear への答え |
| Pabal → ボット | {"event":"buffer_full","dropped_ms":N} | ボットの音声が 30 秒を超えてたまり、あふれた分を捨てた |
| Pabal → ボット | {"event":"stop","reason":"…","played_ms":N,"duration_ms":N} | 通話が終わった。まもなく 1000 で閉じます |
| ボット → Pabal | {"event":"ready"} | start のあと 3 秒以内に、いちばん最初に |
| ボット → Pabal | バイナリ | ボットの音声:偶数バイト、2〜65,536 バイト。live より前のものは捨てられます |
| ボット → Pabal | {"event":"clear"} | 割り込み:並んでいるボットの音声をいま止める |
| ボット → Pabal | {"event":"mark","name":"[A-Za-z0-9_.:-]{1,64}"} | 「ここまで再生されたら知らせて」。通話ごとに 256 個まで待機 |
| ボット → Pabal | {"event":"hangup"} | 通話を終える。並んでいる音声を最大 5 秒先に流してから切ります |
- 知らない
eventは無視されます。JSON ではないテキスト、4KiB を超えるテキスト、奇数バイトや 64KiB を超える音声は仕様違反として通話を終えます(発信者はおわびの案内を聞きます)。 - 送信量の上限:テキストのイベントは平均で毎秒 50 個(瞬間 100 個)、音声は平均で実時間の 4 倍(瞬間 60 秒分)。ボットのソケットが読まず、発信者の音声が 30 秒(1,500 個)を超えてたまった場合も通話を終えます。
署名
ゲートウェイは、アップグレードのリクエストにこのヘッダーを付けます。
X-Pabal-Voice-Signature: t=<Unix 秒>,nonce=<16 バイトの hex>,call=<call_id>,v1=<hex>
v1 = HMAC-SHA256(stream_secret, "pabal-voice-stream/1\n<t>\n<nonce>\n<call>\n<host>\n<path?query>")
hostは自分が Pabal に伝えた URL のホスト(小文字、ポートなし)です。リクエストのHostヘッダーで計算してはいけません — その値は送る側が書くものなので、ホストに結び付ける効果がありません。path?queryは URL のパスとクエリそのまま(なければ/)。プローブではcallがプローブの番号です。- ボットのサーバーは必ず確認します:
|現在 − t| ≤ 300 秒、同じnonceは 1 回だけ、HMAC は処理時間が一定の関数でバイトどうし比較。誤っていれば 401 のように 101 以外の答えを返します。 - そして
start.call_idが署名のcallと同じかを確認します。違えば 1008 で閉じます — 署名されたアップグレードを別の通話に使い回させないためです。1 つのボットだけに仕えるサーバーなら、start.bot_idが自分のボットかどうかも確認します。 - テスト用の値:シークレット
example-stream-secret-of-the-bot-developer-0001、t=1790000000、nonce00112233445566778899aabbccddeeff、call4611686018427387905、hostbot.example.com、パス/voice?bot=100042→v1=807517ff96ac75223bde35b48c775f49a2eda2fb4832a02c0ea832b914d70d0c。
音声 · 割り込み · mark · 終了
- ボット → 発信者のバッファー:ゲートウェイが再生の速さで送り出すので一度に送ってもかまいませんが、30 秒を超えてたまるとあふれた分は捨てて
buffer_fullを送ります。ボットの音声が 300 ms 途切れると、1 つの「かたまり」が終わったとみなします(mark・clearの単位)。 clear:まだ流れていないボットの音声をすべて捨て、次の 10 ms から静かになります(計測で 50 ms 未満)。答えのclearedのplayed_msはこの通話でこれまで実際に再生されたボットの音声の合計、dropped_msは今回捨てた量、marksは捨てられたかたまりに付いていた mark の名前です — モデルに「どこで切れたか」を伝えるのに使います。mark:その前の音声が再生されると、同じ名前で戻ってきます。mark の前の音声は 480 バイト(10 ms)の倍数で終えるのがよいです(余った分は無音で埋められます)。- 終了:ボットは
hangupを送るか、1000・1001 できれいに閉じます。それ以外の閉じ方や TCP の切断は、仕様違反と同じように終わります(発信者におわびの案内)。Pabal が終えるときのstop.reasonは短いコードです —hangup(発信者が切った)、max_length、bot_ended、disconnect、drain、kill_switch、operator、link_lost、media_failed… 知らない値は「終わった」として扱ってください。 - プローブの
startは"probe": true、空のcaller、max_seconds: 0です。readyを送ると{"event":"stop","reason":"probe"}が来て閉じます。プローブでモデルのセッションのような費用のかかるものを開かないでください。
例 — いちばん小さなストリームサーバー
AI なしで仕様全体を確かめるサーバーです:live で通知音を流し、そのあとは発信者の音声をそのまま返します(エコー)。署名・時刻・nonce・通話・ボットをすべて確認します。まずこれで通話が最後まで通ることを確かめてから、エコーの部分に自分の AI を入れてください。Pabal のリポジトリには scripts/bots/stream_echo_bot.py(割り込みのデモ付き)と scripts/bots/stream_openai_relay.mjs(自分の OpenAI のキーで Realtime に中継する、実行していない例)もあるので、必要ならサーバーの運営者に頼んでください。
# stream_bot.py — pip install "websockets>=13"
# 実行: STREAM_SECRET_FILE=stream-secret.txt TLS_CERT=fullchain.pem TLS_KEY=privkey.pem \
# PUBLIC_HOST=bot.example.com BOT_ID=<ボット ID — トークンの : の前の数字> python3 stream_bot.py
import asyncio
import hashlib
import hmac
import json
import math
import os
import ssl
import struct
import time
from websockets.asyncio.server import serve
SECRET = open(os.environ["STREAM_SECRET_FILE"], "rb").read().strip()
PUBLIC_HOST = os.environ["PUBLIC_HOST"].lower() # Pabal に伝えた stream_url のホスト(Host ヘッダーではない)
BOT_ID = os.environ.get("BOT_ID", "") # 空ならどのボットの通話でも受ける
PATH = os.environ.get("STREAM_PATH", "/voice")
PORT = int(os.environ.get("PORT", "8443"))
CHIME = b"".join(struct.pack("<h", int(8000 * math.sin(2 * math.pi * 880 * i / 24000)))
for i in range(24000 * 6 // 10)) # 880 Hz 0.6 秒、PCM16 24 kHz
seen = {} # 一度使った nonce
def signed_call(header, target):
"""X-Pabal-Voice-Signature を確認し、署名された call id を返す。誤っていれば None。"""
try:
parts = dict(p.split("=", 1) for p in header.split(","))
t, nonce, call, v1 = int(parts["t"]), parts["nonce"], parts["call"], parts["v1"]
except (KeyError, ValueError):
return None
now = time.time()
if abs(now - t) > 300 or nonce in seen:
return None
text = f"pabal-voice-stream/1\n{t}\n{nonce}\n{call}\n{PUBLIC_HOST}\n{target}"
expected = hmac.new(SECRET, text.encode("utf-8", "surrogateescape"), hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected.encode(), v1.encode("utf-8", "surrogateescape")): # バイトどうし
return None
for old in [n for n, at in seen.items() if at < now - 600]:
del seen[old]
seen[nonce] = now
return call
def check(connection, request):
if request.path.split("?", 1)[0] != PATH:
return connection.respond(404, "not here\n")
call = signed_call(request.headers.get("X-Pabal-Voice-Signature", ""), request.path)
if call is None:
return connection.respond(401, "bad signature\n")
connection.signed_call = call # start はこの通話を指していなければならない
return None
async def handle(ws):
if ws.subprotocol != "pabal.voice.v1":
await ws.close(1002, "pabal.voice.v1 only")
return
start = json.loads(await asyncio.wait_for(ws.recv(), 5))
if (start.get("event") != "start" or start.get("call_id") != ws.signed_call
or (BOT_ID and start.get("bot_id") != BOT_ID)):
await ws.close(1008, "not this call")
return
await ws.send(json.dumps({"event": "ready"})) # 3 秒以内に、いちばん最初に
live = False
async for message in ws:
if isinstance(message, bytes):
if live:
await ws.send(message) # 発信者の 20 ms をそのまま返す(エコー)
continue
event = json.loads(message)
if event.get("event") == "live" and not start.get("probe"): # AI の案内が終わった
live = True
await ws.send(CHIME)
await ws.send(json.dumps({"event": "mark", "name": "chime"}))
elif event.get("event") == "stop":
print("終了:", start["call_id"], event.get("reason"), flush=True)
break
async def main():
tls = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
tls.minimum_version = ssl.TLSVersion.TLSv1_2
tls.load_cert_chain(os.environ["TLS_CERT"], os.environ["TLS_KEY"])
async with serve(handle, "0.0.0.0", PORT, ssl=tls, subprotocols=["pabal.voice.v1"],
process_request=check, max_size=65536, compression=None):
print(f"待ち受け中: wss://{PUBLIC_HOST}:{PORT}{PATH}", flush=True)
await asyncio.get_running_loop().create_future()
asyncio.run(main())
発信者に見えるもの
- 通話ボタン:音声ボットのチャットの上部とプロフィールに電話のボタンが表示されます。サーバーがその人・その端末について通話を受けられると判断したときだけ表示されます(一般公開の前はテスターにだけ)。ビデオ通話はありません。
- 確認画面(マイクの権限より前):タイトル「An AI answers this call」(この通話には AI が応答します)、ボットは人ではなく声も AI が作ったものであること、エンドツーエンド暗号化ではないこと、音声を受け取る先(Google、Vertex AI の Gemini Live API)と国・地域(United States (Iowa, Google Cloud us-central1))、送るもの(通話中の声、会話の文章と AI の返答、ボットが通話中に入れたメモとボットの機能の結果)、保管(Pabal は音声を保存せず通話記録だけを 180 日、Google は返答を作ったあと保管しないが、自動検査で不正利用が疑われた内容は最大 90 日間記録されることがある)、ボットのオーナーが受け取るもの(名前・アカウント番号・言語、通話の時刻・長さ、ツールに使われた値 — 電話番号と音声は含まない)、19 歳以上。ボタンは「I'm 19 or older · Call」(19 歳以上です · 通話)とキャンセル。年齢は本人確認ではなく自己申告です。通話ボタンがメインのボタン(アクセントカラーで塗りつぶし)で、キャンセルがサブのボタンです。会話を残す機能が有効なサーバーでは、チェックボックス「Keep this call's conversation as text in my chat」(この通話の会話を自分のチャットに文字で残す、既定はオフ)が説明のあとに付きます。
- どのくらいの頻度で:デスクトップ・Android はそのボットに初めてかけるとき(送り先や画面の版が変わるともう一度)、iPhone と Apple Watch は毎回尋ねます。ウォッチは毎回画面の全体を表示し、会話を残すスイッチの初期値は、そのボットにかけた最後の通話での選択です。アプリは通話ごとに同意をサーバーに送り、サーバーは同じ端末・同じボット・2 分以内の同意 1 つを、その通話に 1 回だけ使います。
- つながると:AI の案内音声「You're now talking to an AI voice bot.」(ストリームモードのボットなら「This is an AI voice bot. The call's audio goes to the bot developer's server.」) →(文字起こしのボットなら)文字起こしの案内 → ボットのあいさつ。案内はボットのオーナーがオフにできず、案内が終わる前の発信者の言葉はモデルに送られません。会話を残す機能には案内音声がありません — 発信者が画面で自分で選んだからです。
- 案内の言語:案内音声は、ボットの
languageではなく発信者が読んだ確認画面の言語で流れます。1 つの通話の案内はすべて 1 つの言語です — AI の案内、文字起こしの案内、接続が切れるときのお詫び(ストリームモードのボットなら、ストリームモードの案内とお詫び)。そのうち 1 つでも、その言語の確認済みの録音がなければ、すべて英語で流れます。いま確認済みの言語は韓国語と英語だけです。残りの 8 言語(日本語・中国語・フランス語・スペイン語・インドネシア語・ベトナム語・クメール語・アラビア語)の確認画面と案内は、法的な確認とネイティブスピーカーによる確認を待っている草案なので、その言語のアプリを使う発信者も、いまは英語の確認画面を見て英語の案内を聞きます。クメール語の録音と、日本語の「Just a moment longer, please.」(日本語版の草案は「もう少々お待ちください。」)は、まだ人が聞いて選ぶのを待っています。通話の最初の案内と文字起こしの案内の前には、必ず 300 ms 以上の無音があります — 電話の経路で最初の言葉が欠けないようにするためです。 - 通話画面:名前の下に「@{username} · AI · Bot」と「AI-generated voice · Call audio is processed by Google」(AI が作った声 · 通話の音声は Google が処理します)。絵文字のキーと暗号化の文言はありません。
- 接続に問題が起きると「Sorry, we've lost the connection. Ending the call now.」(申し訳ありません。接続に問題が起きたため通話を終了します)を聞いて切れます。拒否はすべて話し中で終わります。
アプリの利用者向けの説明は、アプリの使い方 — 音声ボットに電話するにあります。
ボットからの電話
サーバーには実装されていますが、プライバシーポリシーの bot-calls-outbound 節と利用規約の voice-bot-owners-outbound 節が法的な確認を終えて掲載されるまでは、どのボットも人に電話をかけられません。許可画面の入ったアプリのビルドもまだ出ていません(現在の状態)。以下は、この機能が有効なサーバーでの動きです。
音声ボットは、人に自分から電話をかけることもできます。予約の確認、決めた時刻に起こす電話、配達のお知らせです。ボットオーナーのコードが startVoiceCall で頼むと、サーバーが判定して相手の端末を鳴らし、相手が出ると Pabal の案内音声のあとに AI 音声モデルがボットの指示文どおりに話します。どう終わったかは voice_call アップデートで届きます。
- 許可した人にだけかけます。相手が Pabal アプリでそのボットに電話を許可する必要があり(着信者の許可)、許可はいつでもオフにできます。
- 広告の電話ではありません。目的はリマインダー・起床・配達・予約の 4 つで、広告・販売の勧誘・宣伝はできません(ボットオーナー規約の 12–24 項)。
- ボットからの電話はエンドツーエンド暗号化ではありません — 音声は Pabal サーバーと AI の提供者を通ります。相手が出ると、通話中に音声は Google に送られます — Vertex AI の Gemini Live API、米国アイオワ(Google Cloud
us-central1)。Pabal は音声を保存しません。 - ボットオーナーに会話の文字は届きません。ボットがかけた電話には文字起こしがありません。ボットオーナーが受け取るのは着信者が見るもの・聞くものに書いた 5 つです。
流れ
図の説明
- 4 本のレーン:左からボットオーナーのコード、Pabal サーバー、音声ゲートウェイと AI モデル、着信者のアプリです。縦の点線が時間で、青い実線が進む流れ、灰色の線がボットへの答えです。
- ①の灰色の枠 — 判定は 3 回あります:答える前にボット側(有効にしたか、運営者の審査、規約、上限)と許可があるかを、200 のあとに相手側(着信しない時間帯、一時停止中、相手の 1 日の上限と間隔、鳴らせる端末)を、鳴らす直前にもう一度確かめます。
- ②の 200 は「受け付けた」という意味だけです。鳴ったか、出たかは⑧のアップデートでしかわかりません。
- ④–⑦ 相手側:着信にはボット名のあとに「(AI)」が付き、出ると Pabal の案内音声(⑥)がボットより先に流れます。ボットは最初のひと言(⑦)で名前、AI であること、電話の目的を伝えます。緑の枠の間、音声はゲートウェイと AI モデルを通ります — エンドツーエンド暗号化ではありません。
- 赤い点線が要点です:相手側の判定で鳴らなかった依頼と、鳴ったのに出なかった依頼は、同じ時刻に同じ
not_answeredで終わります。相手がそのとき寝ていたのか、別の通話中だったのか、ボットにはわかりません。
有効にする — @BotFather
ボットからの電話はボットオーナーが @BotFather でだけ有効にします。トークンではできません(setVoiceCallSettings に発信の項目を送ると 400 VOICE_FIELD_NOT_AVAILABLE)。先に音声通話が有効になっている必要があります。/mybots → ボット → 音声通話(음성 통화)→ ボットからの電話(봇이 거는 전화)→ オン(켜기)。
BotFather は韓国語で答えます。1 つ目のメッセージは、このボットが人に電話できるようにするかを尋ねます:許可した人にだけ、その人の着信しない時間帯と一時停止中は鳴らさず、1 日の上限(今は 50 回)とサーバーの上限の中で、運営者がボットの名前・写真・指示文・声・目的を審査して許可してから(どれかが変わればもう一度)。ボットからの電話はエンドツーエンド暗号化ではありません。2 つ目は有効にしたあとの画面です:オン、運営者の許可待ち、目的は配達、上限 50、今日の依頼数。
- 規約の第 4 版:まだ同意していなければ、有効にする前にボットオーナー規約が表示されます。12–24 項がボットからの電話の約束です。
- 目的:
reminder(リマインダー)・wake_up(起床)・delivery(配達)・appointment(予約)から 1 つ以上選ぶと有効になります。依頼ごとにこのうち 1 つを書き、選んでいない目的は400です。目的を変えると、許可した人にもう一度尋ね、運営者がもう一度審査します。 - 1 日の上限:このボットが 1 日に送れる依頼の数。10・20・50 回から選びます(既定 50)。
- 運営者の審査:運営者が管理画面でボットの名前・@ユーザー名・写真・指示文・あいさつ・ツール・声・言語・モード・目的・オーナーの名前を見て許可すると、かけられるようになります。どれか 1 つでも変われば、もう一度許可されるまでかけられません(
403 VOICE_OUTBOUND_OFF: review)。 - トークン:トークンで音声の設定を変えると(音声をオフにするのは除く)、発信が 24 時間止まります(
frozen)。自分で変えたのなら @BotFather の自分で変えました(내가 바꿨어요、vdk・vdK)で解除します。トークンを再発行しても 24 時間止まり、進行中の発信は終わり、発信が有効なボットの Webhook は削除されます。 - ボットの言語が韓国語・英語(
ko-KR・en-US)のボットだけがかけられます。ストリームモード・自前のキー・ツール・MCP サーバーを使うボットはかけられません(mode)。
着信者の許可
ボットは許可を自分で得ることはできません。相手が Pabal アプリでそのボットの許可画面を開き、「I am 19 or older · Allow」(19 歳以上です · 許可)を押す必要があります。画面には、AI が電話をかけること、目的、ボットオーナーの名前、音声の送り先、着信しない時間帯が表示されます(アプリの使い方 — ボットからの電話を許可する)。画面を開く場所は 3 つです。
- ボットのメニュー:ボットのチャットやプロフィールの「Allow calls…」(通話を許可…)。
- 1 対 1 の会話の中のリンク:ボットが相手との 1 対 1 の会話に
https://pabal.me/<ボットのユーザー名>?callme=1を本文のリンクかインライン URL ボタンで送ります。そのボットがその会話で送ったメッセージのときだけ画面が開きます — 別の会話に転送されたリンク、プレビュー、Web ではチャットが開くだけです。リンクから画面を開けるのは 1 人 1 日 1 回(すべてのボット合わせて)で、ボットと話したことがなければ開きません。 - 設定の一覧「Bots allowed to call me」(電話を許可したボット):許可したボットを見て、オフにし、もう一度許可します。
curl -s "https://pabal.me/bot$BOT_TOKEN/sendMessage" -d chat_id=$USER_ID \
--data-urlencode "text=ご注文の品が届く前にお電話でお知らせしましょうか?" \
--data-urlencode 'reply_markup={"inline_keyboard": [[{"text": "通話を許可", "url": "https://pabal.me/order_helper_bot?callme=1"}]]}'
- 自前で運用するサーバー:アプリが知っているのは
pabal.meのリンクだけです。別のサーバーなら、アプリのビルドのリンクのルールにそのサーバーのホストを加えないと、リンクから画面は開きません(メニューと設定の一覧はそのまま使えます)。 - 今の範囲:+82 の番号のアカウントで、端末のタイムゾーンが韓国(ソウル)、ログインから 24 時間がたった端末でだけ許可をオンにできます(国は運営者が決めます)。19 歳未満と答えると、1 年間ボットと通話できません。
- ボットの名前・オーナー・目的が変わったり、許可から 1 年たったりすると、もう一度尋ねます。その間はかけられません。
かける — startVoiceCall
curl -s "https://pabal.me/bot$BOT_TOKEN/startVoiceCall" -d user_id=$USER_ID -d purpose=delivery \
--data-urlencode "context=注文 A-1042 は今日の午後 3 時ごろ届きます。そのとき受け取れるか聞いてください。" \
-d max_duration=180 -d ring_timeout=30
{"ok": true, "result": {"voice_call_id": "8812345678901234"}}
| パラメーター | 意味 |
|---|---|
user_id | 電話をかける相手の ID。その人がこのボットに許可している必要があります |
purpose | このボットが選んだ目的の 1 つ:reminder・wake_up・delivery・appointment |
context | モデルにだけ渡す、この電話の説明。1–1,000 文字・3,072 バイト。内容のルール(なりすまし、AI であることを隠す、秘密を求める、広告)を検査します。モデルは最初のひと言(名前・AI・目的)のあと、相手が答えてから使います |
max_duration | 出たあとの最大の長さ(秒)、30 から 300 とボットの上限の小さいほうまで。既定 180 |
ring_timeout | 鳴らす時間(秒)、30–45。既定 40 |
| 答え | 意味 | すること |
|---|---|---|
200 {"voice_call_id": "…"} | 受け付けた。鳴ったか・出たかはアップデートで | — |
400 Bad Request: <内容> | パラメーターの誤り、選んでいない目的(purpose is not declared)、ボットの言語が韓国語・英語でない | 直す |
400 VOICE_CONTEXT_REFUSED: <コード> | context が内容のルールに当たった | 直す |
403 VOICE_OUTBOUND_OFF: <理由> | ボット側:owner_off(有効にしていない)・terms(規約の第 4 版)・review(運営者の審査待ち)・suspended(自動停止)・frozen(トークンによる停止)・owner_name(オーナーの名前がなりすまし検査に当たった)・mode、または音声の設定(unavailable_reason)のボット側の理由:bot_voice_off・not_allowed・suspended・name_refused・text_refused・awaiting_review・stream_unavailable・own_key_unavailable・no_gateway・no_gateway_with_tools・no_gateway_with_language・no_own_key_gateway。サーバー側の理由 voice_off・kill_switch・no_relay・outbound_off は 403 ではなく 503 で届きます | @BotFather・運営者 |
403 USER_UNAVAILABLE | その人にはかけられない:許可がない・オフ・再確認中、ブロック、人ではない、範囲外、19 歳未満、テスターではない。本文は理由にかかわらずいつも同じ | 許可をお願いする |
429 retry after N | ボット・オーナー側の上限:依頼の速さ、ボット・オーナーの 1 日の依頼数、ボットの同時通話、オーナーの発信の分数、ボット・オーナーの 1 日の通話時間(ボットへの電話と共有) | N 秒後 |
503 VOICE_UNAVAILABLE | サーバー側:発信のスイッチがオフ、サーバーの音声がオフ(プライバシーポリシーにボット通話の節がないときを含む)、緊急停止スイッチ、通話中継がない、サーバーの上限と発信の割り当て、かけられるゲートウェイがない(またはすべて満杯)、依頼を記録できなかった | あとで |
{"ok": false, "error_code": 403, "description": "Forbidden: USER_UNAVAILABLE"}
相手側の判定は 200 のあとに行います。許可があっても、着信しない時間帯、一時停止中、その人との 1 日の上限、その人の 1 日の上限と間隔、鳴らせる端末がない、別の通話中なら、着信なしで依頼から ring_timeout + 12 秒ほどのちに not_answered が届きます。鳴ったのに出なかった場合と区別できません。許可があってもボット側の判定(403・429・503)は変わりません。
設定と使用量
"outbound": {
"enabled": true,
"purposes": ["delivery"],
"daily_cap": 50,
"reviewed": true,
"frozen_until": 0,
"suspended": false,
"available": true
}"outbound": {
"today": {"requests": 4, "answered": 2, "not_answered": 2, "minutes": 3},
"limits": {"daily": 50, "owner_daily": 100, "owner_minutes": 30}
}getVoiceCallSettingsのoutboundはいつもあります:@BotFather で決めたもの(enabled・purposes・daily_cap)、運営者の審査(reviewed)、トークンによる停止が解ける時刻(frozen_until、止まっていなければ 0)、自動停止(suspended)、今かけられるか(available)。availableが false のときだけunavailable_reasonが付きます(ボット側なら403 VOICE_OUTBOUND_OFFの理由と同じ値、サーバー側ならvoice_off・kill_switch・no_relay・outbound_off— この 4 つは503 VOICE_UNAVAILABLEに対応します)。getVoiceCallUsageのoutboundは、発信を一度でも有効にしたボットにだけあります:今日の依頼・応答・不応答・分数と、このボットにかかる 1 日の上限。鳴った回数は載せません。calls・seconds・missed・refusedにボットがかけた電話は入りません。ただし、ボットがかけた電話は、ボットへの電話と同じボット・オーナーの 1 日の通話時間を使います:出た通話の秒数が計上されてremaining_seconds_todayのbot・ownerが減り、鳴っている間も予約の分だけ減って、出なければ戻ります。setVoiceCallSettingsでは変えられません(400 VOICE_FIELD_NOT_AVAILABLE)。
アップデート
ボットへの通話と同じ voice_call アップデートで届きます(allowed_updates に voice_call)。direction は "outgoing" で、相手は to の ID 1 つだけで表されます(名前・ユーザー名・電話番号はなし)。
{
"update_id": 83,
"voice_call": {
"id": "8812345678901234",
"direction": "outgoing",
"event": "not_answered",
"to": {"id": 100005},
"purpose": "delivery",
"date": 1790000123
}
}{
"update_id": 81,
"voice_call": {
"id": "8812345678901234",
"direction": "outgoing",
"event": "started",
"to": {"id": 100005},
"purpose": "delivery",
"date": 1790000130,
"max_duration": 180
}
}{
"update_id": 82,
"voice_call": {
"id": "8812345678901234",
"direction": "outgoing",
"event": "ended",
"to": {"id": 100005},
"purpose": "delivery",
"date": 1790000250,
"start_date": 1790000130,
"duration": 120,
"max_duration": 180,
"end_reason": "callee"
}
}{
"update_id": 84,
"voice_call": {
"id": "8812345678901234",
"direction": "outgoing",
"event": "failed",
"to": {"id": 100005},
"purpose": "delivery",
"date": 1790000110
}
}event | いつ | 追加の項目 |
|---|---|---|
started | 相手が出て通話がつながったとき。Apple Watch で出た通話は、ウォッチの音声がつながったとき(つながらなければ 0 秒の ended、failed) | max_duration |
ended | 出た通話が終わったとき。サーバーの設定にかかわらず、ゲートウェイの使用量を待つため最大 10 秒遅れて届きます | start_date、duration、max_duration、end_reason |
not_answered | 出なかった依頼 — 鳴ったのに出ない、拒否、相手側の判定で鳴らなかったを区別しません。依頼から ring_timeout + 12 秒ほど | なし |
failed | 依頼を受け持ったゲートウェイとの接続が、出る前に切れた | なし |
end_reason:callee(相手が切った)・bot(ボットが切った、トークンの再発行)・max_length(最大の長さ)・failed(出たあとの接続失敗)・operator(運営者・緊急停止スイッチ)・suspended(ボット・オーナーの停止)。ボットがかけた電話には missed・refused・ringing がなく — 何秒で出たかで相手が電話のそばにいたかがわからないように — transcript もありません(設計上オフ)。
通話中
endVoiceCall:結果を知らせる前なら、鳴っている間も鳴る前も依頼を取り下げます(true、そのあとnot_answered)。出た通話は終わらせます(end_reasonbot)。依頼から 15 秒以内に取り下げると、その人との組が 1 時間一時停止します。sendVoiceCallContext:相手が出たあとだけ使えます(その前はVOICE_CALL_NOT_FOUND)。1 通話 3 回、1,000 文字・3,072 バイト、contextと同じ内容のルール。ゲートウェイがメモを受け付けなければVOICE_CONTEXT_NOT_SUPPORTED。
着信者が見るもの・聞くもの
- 着信:ボット名のあとに「(AI)」が付き、「AI voice bot · Pabal never asks for codes」(AI 音声ボット · Pabal は認証コードを尋ねません)が表示されます。
- 出ると案内音声がボットより先に流れます:「This is a Pabal AI voice call. We never ask for passwords, codes or money transfers. To stop these calls, turn off call permission in the bot's chat.」(Pabal の AI 音声電話です。パスワード・認証コード・送金は決して尋ねません。止めるにはボットのチャットで通話の許可をオフにしてください)— 相手が読んだ許可画面の言語(韓国語・英語)で流れ、ほかの言語のアプリなら英語です。ボットオーナーはオフにできません。
- ボットの最初のひと言:ボットの名前、AI であること、電話の目的。
contextはそのあとに使います。 - オフにする方法:メニューの「Stop calls from this bot」(このボットからの通話を止める)、ボットのブロック、通報はいつでもオフにします。通話中に「もう二度とかけないで」のようにはっきり言えば、その場でオフになります(ただの「やめて」はモデルの判断次第です)。
- 一時停止中:拒否する、鳴ったのに出ない、出て 10 秒以内に切ると、そのボットは 24 時間その人にかけられません。
- 不在着信の記録:端末が実際に鳴り、ボットをブロックしておらず、そのボットのチャットに未読のメッセージがないとき、「Missed AI bot call」(AI ボットからの不在着信 — 既読、通知なし)として残ります。未読のメッセージがあるか、着信しない時間帯・一時停止中で鳴らなかったなら記録はありません。ボットがした依頼はすべて、通報の証拠(30 日)には漏れなく残ります。
- ボットオーナーに届くもの(許可画面と同じ順番):
- 相手の Pabal ID
- 許可がオンかどうか(電話の依頼への答えでわかる)
- 出たかどうか
- 通話の開始時刻と長さ
- 誰が切ったか
- 共有の端末:ログインしている端末ならどこでも出られ、出たのが誰でもボットは目的と案内の内容を話します。何人かで使うパソコンならログアウトするよう伝えてください。
- ロック画面:Android 14 以降では、全画面通知を許可していなければ、ロック画面に通話画面ではなく通知として届きます。iPhone と Apple Watch は電話のように鳴ります。
- 会話を残すは、相手が許可画面で選べば相手のチャットにだけ残ります。Pabal ウォッチアプリでウォッチで受けるをオンにすると Apple Watch も一緒に鳴ります。
- ボットからの電話はエンドツーエンド暗号化ではありません — 音声は Pabal サーバーと AI の提供者を通ります。
上限 · 着信しない時間帯 · 一時停止 · 自動停止
| 上限 | 既定 | 本番サーバーでは |
|---|---|---|
| 1 つのボットから 1 人へ(1 日) | 3 回 | 運営者は下げることだけできる |
| 1 人へ(1 日、すべてのボット) | 6 回 | 下げることだけできる |
| 1 人へかける間隔 | 10 分 | 上げることだけできる |
| 1 つのボットの依頼(1 日) | 50 回 | オーナーが @BotFather で 10・20・50 から選ぶ |
| ボットオーナー 1 人(1 日、すべてのボット) | 100 回 · 30 分 | 回数は下げることだけできる |
| ボットが次の依頼を送れるまで | 10 秒 | 上げることだけできる |
| サーバーの発信の分数 | サーバーの一日の分数の 30% | 下げることだけできる |
| 着信しない時間帯 | 21:00–08:00 | 運営者と相手は広げることだけできる。端末のタイムゾーン基準 |
起床通話の時間帯(wake_up のみ) | 最大 120 分 | 相手が着信しない時間帯の中で選ぶ |
着信(ring_timeout) | 30–45 秒 | 依頼で指定がなければ 40 秒 |
| 一時停止 — 拒否、鳴ったのに出ない、出て 10 秒以内に切る | 24 時間 | 上げることだけできる |
| 一時停止 — ボットが 15 秒以内に取り下げた | 1 時間 | 上げることだけできる |
- 「1 日」はサーバーのタイムゾーン(
TELEGRAM_VOICE_TIME_ZONE)の 1 日です。通話の長さは 300 秒を超えません。 - ボットからの電話も、ボットへの電話と同じ 1 日の通話時間(ボット 60 分・オーナー 120 分)を使います。使い切ると
startVoiceCallは429を受け、そのボットへの電話もrefused(budget)で断られます。 - 一時停止が解けるとき:時間がたった、相手がそのボットに電話した、相手が「I can take calls now」(今なら受けられます)を押した、もう一度許可した。ボットのメッセージやボタンでは解けません。
- 自動停止はそのボットの発信だけを止めます(
403 VOICE_OUTBOUND_OFF: suspended)。運営者が確認して解除するまで続きます。条件:直近 20 回で応答率が 20% 未満、7 日以内に鳴らした相手からの通報が 3 件、許可をオフにした人が 30% 以上(10 人以上のとき)、通話中にルールに当たったのが 2 件、15 秒以内の取り下げが 5 件、出た人は 2 人以下なのに応答した分数は 60 分以上。
上限
| 上限 | 既定 | 誰が決めるか |
|---|---|---|
| 1 回の通話の最大の長さ | ボット 300 秒(設定できる範囲は 60 〜 運営者・サーバーの上限、既定 600) | ボットのオーナーが運営者・サーバーの上限の範囲で。@BotFather のボタンは 1・3・5・10 分 |
| ボットの同時通話 | 2(1 〜 運営者・サーバーの上限、既定 2) | ボットのオーナーが運営者・サーバーの上限の範囲で |
| ボット 1 つの 1 日の通話時間 | 60 分 | 運営者 |
| ボットのオーナー 1 人(すべてのボットの合計)の 1 日 | 120 分 | 運営者 |
| サーバー全体の 1 日 | 300 分 | 運営者 |
| 発信者 1 人 | 1 日 60 分、1 時間に 20 回 | 運営者 |
| 受け付ける最短の通話 | 60 秒 — 残り時間がこれより少なければ受けない | 運営者 |
| 新しいアカウント | 登録から 7 日たっていない人は電話できない — テスターと、ボットが 1 日以上前にその人にメッセージを送ったことのある人は例外 | 運営者 |
| 設定の変更 | ボットごとに 1 時間 30 回 | — |
| 通話中のメモ | 通話ごとに 1 分に 10 件 | — |
| ツール | 16 個・8KB、開いている問い合わせは通話ごとに 2・ボットごとに 8、1 分に通話ごとに 30・ボットごとに 120 | — |
| 自前のキーのテスト · ストリームのプローブ | ボットごとに 1 時間 5 回、オーナーごとに 10 回 | — |
| MCP の登録・検査 | ボットごとに 1 時間 10 回、オーナーごとに 20 回、ボットごとにサーバー 2 つ | — |
Apple Watch からは、ツール・MCP サーバー・自前のキー・ストリームモード・ボットのオーナーの文字起こしを使うボットにかけられません。この拒否は発信者(ウォッチアプリの 403 WATCH_NOT_YET)にだけ届き、ボットには refused アップデートは届きません。
1 回の通話の長さは、発信者・ボット・オーナー・サーバーの今日の残り時間と通話の上限のうち、いちばん小さいものです。受けるときにその分を 4 つの上限すべてに確保し、終わるとサーバーの時計で実際の秒数だけを請求します。自前のキーの通話は、ボット・オーナー・サーバーの上限を使いません(発信者の上限はそのまま)。
エラー
エラーはほかのメソッドと同じ形です:{"ok": false, "error_code": 400, "description": "Bad Request: VOICE_…"}。403 は Forbidden: <コード>: <説明>(USER_UNAVAILABLE だけは説明なしでいつも Forbidden: USER_UNAVAILABLE)、429 には parameters.retry_after が付きます。
| コード | 意味 |
|---|---|
403 VOICE_RULES_NOT_ACCEPTED | @BotFather で先に音声ボットのルールに同意する必要がある |
403 VOICE_NOT_AVAILABLE | このサーバーではこのボットは音声を使えない(音声が無効・運営者が許可していない)— 設定を変えられず、無効にする操作と削除だけができる |
403 VOICE_SUSPENDED | 運営者がこのボットの音声を停止した。無効にする操作だけができる |
403 VOICE_OWNER_TERMS_NOT_ACCEPTED | 先にボットオーナー規約(第 4 版)に同意する必要がある |
403 VOICE_OWN_KEY_NOT_AVAILABLE | このサーバーは自前のキーを受け付けない |
403 VOICE_STREAM_NOT_AVAILABLE | このサーバーではいまストリームモードを使えない |
403 VOICE_OUTBOUND_OFF: <理由> | このボットは今、人に電話をかけられない:owner_off・terms・review・suspended・frozen・owner_name・mode、または音声の設定のボット側の理由(かける)。voice_off・kill_switch・no_relay・outbound_off は 503 |
403 USER_UNAVAILABLE | その人にはかけられない — 許可がない場合も含め、理由にかかわらずいつも同じ本文 |
503 VOICE_UNAVAILABLE | サーバー側の事情で今は人に電話をかけられない(発信のスイッチ、サーバーの音声がオフ、緊急停止スイッチ、通話中継がない、サーバーの上限、ゲートウェイ、依頼の記録の失敗)— あとでもう一度 |
Bad Request: purpose is not declared、Bad Request: the bot's voice language must be ko or en for outbound calls | startVoiceCall:選んでいない目的、韓国語・英語でないボットの言語 |
VOICE_INSTRUCTIONS_REQUIRED、VOICE_INSTRUCTIONS_TOO_LONG、VOICE_GREETING_TOO_LONG | 指示文が必要 · 指示文が 8,192 バイト · あいさつが 512 バイトを超えた |
VOICE_INVALID、VOICE_LANGUAGE_INVALID、VOICE_LIMIT_INVALID、VOICE_MODE_INVALID | ない声 · 言語 · 範囲外の上限 · managed/stream ではないモード |
VOICE_CONTENT_REFUSED: <項目>: <コード>、VOICE_CONTEXT_REFUSED: <コード> | 内容のルール(設定、startVoiceCall の context) |
VOICE_TOOL_INVALID: <パス>: <理由> | ツールの定義(ツールを決める) |
VOICE_FIELD_NOT_AVAILABLE: … | 受け付けない項目(model、api_key)、マネージドモードのボットにストリームの項目、ボットからの電話の項目(outbound・purposes・daily_cap — @BotFather でだけ) |
VOICE_CALL_NOT_FOUND、VOICE_CALL_NOT_ACTIVE、VOICE_CONTEXT_NOT_SUPPORTED、VOICE_CONTEXT_TOO_LONG | 通話中のメソッド(通話中に) |
VOICE_USAGE_PERIOD_INVALID | getVoiceCallUsage の期間 |
TOOL_QUERY_ID_INVALID、TOOL_QUERY_EXPIRED: …、TOOL_RESULT_…、TOOL_ERROR_TOO_LONG | ツールの答え(答える) |
API_KEY_INVALID、API_KEY_NO_REALTIME_ACCESS、503 API_KEY_CHECK_UNAVAILABLE、VOICE_PROVIDER_INVALID | 自前のキー(自前のモデルキー) |
STREAM_URL_INVALID、STREAM_SECRET_REQUIRED、STREAM_SECRET_INVALID、STREAM_PROBE_FAILED: … | ストリームモード(設定とプローブ) |
MCP_…、MCP_OPT_IN_REQUIRED | MCP サーバー(登録する)。503 VOICE_NOT_AVAILABLE: mcp: … は少し待ってから再試行、409 MCP_CHECK_IN_PROGRESS は検査が終わってから再試行 |
429 Too Many Requests: retry after N | 1 時間の上限(設定・キーのテスト・プローブ・MCP)、メモの上限、ボットからの電話の上限(上限) |
MTProto で接続するボット
- MTProto だけでログインしたボットは、音声のメソッドを
bots.sendCustomRequestで呼びます(メソッド名と同じ JSON)。使えるもの:setVoiceCallSettings、getVoiceCallSettings、answerVoiceCallToolQuery、endVoiceCall、sendVoiceCallContext、getVoiceCallUsage、startVoiceCall、MCP のメソッド 5 つ。それ以外はMETHOD_INVALID。startVoiceCallの答えは{"voice_call_id": "…"}で、拒否は RPC エラー(USER_UNAVAILABLE、VOICE_OUTBOUND_OFF: review、FLOOD_WAIT_n、VOICE_UNAVAILABLE)で届きます。 - Webhook の応答からは呼べないメソッド:
setVoiceCallApiKey、startVoiceCall、MCP の登録・キーのメソッド。Webhook の POST への応答の本文に入れても実行しません — 人が送ったメッセージ 1 つが Webhook の応答を通じてそのまま電話にならないようにするためです。別のリクエストで呼んでください。 - 自前のキーは HTTPS でのみ:
setVoiceCallApiKey・deleteVoiceCallApiKeyはsendCustomRequestでは呼べません。キーを削除するには HTTP Bot API か @BotFather を使ってください。 - Webhook がなく、直近 15 秒以内に
getUpdatesを呼んでいないボットには、voice_call(ボットがかけた電話のものも)がupdateBotWebhookJSONで 1 回プッシュされます(allowed_updatesに関係なく、再送なし)。ツール問い合わせはupdateBotWebhookJSONQueryで届き、bots.answerWebhookJSONQueryで{"result": …}か{"error": "…"}を答えます。voice_call_transcriptは MTProto では届きません(終わったアップデートのtranscriptは届きます)。 - RPC エラーは、
429がFLOOD_WAIT_n、それ以外は大文字のコードそのままです。