開発者ドキュメント
日本語

ボット開発

音声ボット

アプリからボットに電話をかけると、AI 音声モデルが応答します。有効にする方法、指示文と声、通話の通知、ツール、MCP サーバー、自前のモデルキー、文字起こし、ストリームモード、Apple Watch と会話を残す機能まで — ボットのオーナー向けの案内です。

音声ボットは、アプリから人が電話をかけられるボットです。電話に出るのはボットのオーナーのコードではなく、Pabal の AI 音声モデルです。ボットのオーナーは、そのモデルが従う指示文・あいさつ・声・言語・上限を Bot API で決めます。通話が始まるときと終わるときに voice_call アップデートを受け取り、通話中にモデルからボットのコード(ツール)を呼ばせて、注文を調べたり予約を確認したりできます。AI モデルの代わりに自分のサーバーが音声を直接受け取るストリームモードもあります。初めてボットを作る場合は、先にボットを作るをご覧ください。

現在の状況

音声ボットはサーバーごとに有効にする必要があり、このドキュメントを更新した時点(2026-10-02)で pabal.me では無効です — 開発サーバーで試験中です

下の条件がすべてそろったサーバーでだけ、音声ボットに電話できます。メソッドは音声が無効なサーバーにもあるので、設定を読むことはできます。ただし、このサーバーで音声が有効になり、運営者がそのボットを許可するまでは、無効にする操作(音声を無効にする・文字起こしを無効にする・ストリームモードを無効にする・自前のキーを削除する・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 本の電話の流れ

発信者 Pabal(運営者のサーバー) Pabal の外 Pabal アプリ 確認画面 · 19歳以上 パッチ適用ビルド pabal-server MTProto · Bot API 受付の判定 · 上限 通話記録 オーナーのプログラム Bot API (HTTPS) あなたのコード · サーバー coturn 音声の中継のみ pabal-voice AI の案内音声 · 指示文 音声を保存しない Google Vertex AI Gemini Live us-central1(米国) ① 電話 ② リンク 通話 · 設定 · ツール ③ voice_call · ツール問い合わせ 設定 · ツールの回答 · endVoiceCall 音声(中継のみ) ④ 音声 ↔ AI の返答
マネージド通話:上が信号、下が音声

図の説明

  • 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 のどちらでもできます。

  1. 運営者にボットの許可を頼みます。許可したボットだけを受け付けるサーバー(既定)では、運営者が管理画面の音声タブでボット ID(トークンの : の前の数字)を許可するまで、@BotFather に音声通話のボタンがなく、設定を変えると 403 VOICE_NOT_AVAILABLE になります。一般公開の前なら、試しに電話をかける自分のアカウントもテスターに入れてもらってください(自分のユーザー ID は、ボットにメッセージを送ったあとの getUpdates の message.from.id)。
  2. 指示文を決めます。ボットが電話で何をするかを書いた文章です。下の setVoiceCallSettings で決めるか、@BotFather の音声通話の画面で指示文(지시문)ボタンから送ります。指示文がないと有効にできません(ストリームモードは例外)。
  3. @BotFather で /mybots → ボット → 音声通話(음성 통화) → オンにする(켜기)。最初の 1 回は音声ボットのルールが表示され、ルールに同意してオンにする(규칙에 동의하고 켜기)を押すと有効になります。次からは「オンにしますか?」(켤까요?)と聞かれるだけです。
  4. 再許可を待ちます。許可したボットだけを受け付けるサーバーでは、許可はその時点の指示文・あいさつ・ツールを許可したことになるため、許可のあとで指示文を書くと unavailable_reason: "awaiting_review" になります。運営者が確認して再び許可すると、電話を受けられるようになります。3 つのうちどれか(ストリームモードならストリームのアドレス)を変えるたびに同じです。ボットの言語が韓国語・英語以外なら、すべてのボットを受け付けるサーバーでも、運営者が指示文・あいさつを読んで許可するまで電話を受けません(声と言語)。
  5. getVoiceCallSettings の available が true であることを確かめ、確認画面を含むアプリのビルドで、ボットのプロフィールの通話ボタンから電話してみます。
켜기
BotFather음성 통화를 켜기 전에 음성 봇 규칙을 확인해 주세요. 1. 봇은 언제나 AI 라고 밝혀요. 사람인 척하게 할 수 없어요. 2. 실제 기관·회사·사람을 사칭할 수 없어요. 3. 카드 번호·주민등록번호·비밀번호·인증 번호를 묻게 할 수 없어요. 4. 통화 소리는 파발이 저장하지 않고, 대답을 만들기 위해 Google(Vertex AI)로 보내져요. 5. 봇과의 음성 통화는 만 19세 이상만 할 수 있어요. 6. 규칙을 어기면 운영자가 음성 통화나 봇을 멈출 수 있어요.
규칙에 동의하고 켜기
BotFather음성 통화를 켰어요. 음성 통화 · @order_helper_bot 상태: 켜짐 목소리: Kore (단단한) · 언어: 한국어 … 전화를 받으면 먼저 "AI 음성 봇과 연결되었습니다" 안내가 나가고, 그다음 봇이 인사해요.

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}
パラメーター型説明
enabledBooleanオン・オフ。オンにするには @BotFather でルールに同意済みである必要があり(そうでなければ 403 VOICE_RULES_NOT_ACCEPTED)、マネージドなら指示文が必要です。オフにするのはいつでもできます — いま進行中の通話は最後まで続き、新しい電話だけを受けなくなります
instructionsStringモデルが従う指示文。UTF-8 で 8,192 バイト(日本語で約 2,700 文字)まで。"" は削除 — 有効なマネージドモードのボットでは空にできません。長いほど通話の料金が増え、発信者に知られる可能性があるので、パスワードやキーは入れないでください
greetingStringAI の案内のあとにボットが最初に言う言葉。512 バイト(日本語で約 170 文字)まで。"" ならモデルが自分で短くあいさつします
voiceString声の id(一覧)、既定は kore
languageStringボットが話し、聞き取る言語で、9 つの id のどれかです(既定は ko-KR、一覧)。zh-CN は cmn-CN として、大文字・小文字だけが違う id(ja-jp)は一覧の表記で保存します。韓国語・英語以外なら、運営者が指示文・あいさつを読んで許可したあとで電話を受けます。冒頭の AI の案内音声はこれではなく、発信者が見た確認画面の言語で流れます
max_call_durationInteger1 回の通話の最大の長さ(秒)。60 以上、limits.max_call_duration(運営者とサーバーの上限のうち小さいほう、既定 600)以下。ボットの既定値 300
max_concurrent_callsInteger同時に受ける通話の数。1 以上、limits.max_concurrent_calls(既定 2)以下。ボットの既定値 2
toolsArray通話中にモデルが呼べるボットのツール(ツール)。[] ですべて削除
transcriptBoolean文字起こしのスイッチ(文字起こし)
mode、stream_url、stream_secretStringストリームモード(ストリームモードの設定)
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_reasonMCP サーバー
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_after 60)。

使用量 — 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 ツールを呼び、サーバーがその問い合わせをボットに渡し、ボットの答えをモデルが言葉で伝えます。ツールはボットのオーナーが定義し、ボットのコードが答えます。

モデル · 音声ゲートウェイ Pabal サーバー ボットのコード ① check_order {"order_id": "A-1042"} 引数検査 · 余分なキー削除 カード·ID 番号はここで停止 ② voice_call_tool_query · budget_ms 無音: "One moment, let me check." 5秒: "Just a moment longer, please." ③ answerVoiceCallToolQuery(result) 回答検査 · 秘密·カード拒否 16,384 バイトまで ④ tool_result(データとしてのみ) モデルが結果を言葉で伝える 遅延時: "The tool did not answer in time."
ツール問い合わせ 1 件:モデル → サーバー → ボット → サーバー → モデル

図の説明

  • 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_ms1,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)。

  1. Webhook があれば Webhook で。Webhook の allowed_updates に voice_call_tool_query が必要で、なければほかの経路には回らず届きません(tool_delivery_problem: "not_in_allowed_updates")。問い合わせは別に送る POST なので、順番待ちのキューにふさがれることはなく、再送もしません。
  2. Webhook がなく、直近 15 秒以内に getUpdates を呼んでいれば、次の getUpdates の先頭に。こちらも allowed_updates に入っている必要があります。1 つのボットで待機できる問い合わせは 8 件まで。
  3. それ以外で、MTProto で接続しているボットなら updateBotWebhookJSONQuery で(allowed_updates に関係なく)。
  4. どれにも当たらなければ届かず(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봇 주인 약관을 확인해 주세요. 대화 기록, 자기 모델 키, 원음 모드, MCP 서버, 봇이 거는 전화에 필요해요. 1. 대화 기록을 켜면, 통화가 끝날 때 AI가 적은 대화 글을 내가 받아요. 받은 글은 내가 처리하고 지켜요. 2. 대화 기록이 켜진 봇에 전화하면, 통화가 연결될 때 안내 음성이 대화 글이 봇 주인에게 간다고 먼저 알려요. 3. 받은 대화 글은 필요한 만큼만 두고, 발신자가 지워 달라고 하면 지워요. 모델 학습에 쓰지 않아요. 4. 자기 모델 키를 쓰면, 통화 소리와 글이 내 키의 제공자에게 내 계약으로 가요. 그 처리의 책임은 나에게 있고, 내 개인정보 안내에 그렇게 적어요. 5. 원음 모드에서는 전화 소리가 파발의 AI 모델이 아니라 내 서버로 가요. 그 소리를 받아 처리하는 건 나예요. 전화를 건 사람의 소리와 말한 내용을 법이 허락하는 범위에서만 쓰고, 필요 없으면 지워요. 6. 내 서버가 어디에 있는지, 무엇을 하는지 전화를 건 사람이 물으면 알려요. 봇 소개에 적어 두면 좋아요. 7. 봇은 언제나 AI 라고 밝혀요. 대답은 AI 가 만들어요. 사람이 직접 대답하거나, 사람의 녹음을 AI 대답처럼 틀지 않아요. 원음 모드에서는 파발이 통화 맨 앞에 "AI 음성 봇입니다. 통화 소리는 봇 개발자의 서버로 갑니다." 안내를 틀어요. 8. 카드 번호·주민등록번호·비밀번호·인증 번호를 묻지 않아요. 음성 봇 규칙도 그대로 지켜요. 9. 파발은 대화 글을 저장하지 않아요. 전해지면, 늦어도 24시간 뒤에는 서버에서 지워져요. 키, 원음 모드와 MCP 서버의 비밀은 암호화해 두고 어디에도 기록하지 않아요. 10. MCP 서버를 등록하면, 통화 중 모델이 그 서버의 도구를 부를 때 발신자가 말한 값이 그 서버로 가요. 그 서버와 그 서버가 넘기는 곳의 처리 책임은 나에게 있고, 받는 곳은 사실대로 신고해요. 11. 약관을 어기면 운영자가 음성 통화나 봇을 멈출 수 있어요. 봇이 거는 전화를 켜려면 아래도 지켜요. 12. 봇이 거는 전화는 선언하고 운영자가 허락한 목적(알림·깨우기·배달·예약)에만 써요. 광고·판매 권유·홍보를 하지 않아요. 배달·예약 안내 안에서도 마찬가지예요. 광고성 정보를 보내면 정보통신망법 제50조를 어긴 것이고 그 책임은 나에게 있어요. 파발은 그런 발신을 곧바로 멈추고 발신을 해지해요. 진료·채용·대출처럼 사람의 권리나 의무에 관한 판단을 이 전화로 알리지 않아요. 13. 전화를 건 봇은 첫 말에서 봇 이름과 AI 라는 것을 밝히고, 사람·기관·파발 직원을 사칭하지 않아요. 14. context 에 건강·금융(금액·계좌)·주소 같은 민감한 정보를 넣지 않고, 비밀번호·인증 번호·카드 번호를 묻거나 권하지 않아요. 15. 만 19세 미만인 사람에게 전화하려 하지 않아요. 16. 조용한 시간·쉬는 중·하루 한도를 피하려 하지 않아요(여러 봇·계정으로 나눠 걸기 포함). 사람이 그만 받겠다고 하면 다시 걸지 않아요. 17. 내 개인정보 처리방침을 두고, 통화 내용이 AI 제공자에게 간다는 것을 알려요. 봇이 거는 전화는 종단 간 암호화가 아니에요. 18. 사람에게 보이는 허락 창에 봇 주인인 내 이름이 나와요. 19. 어기면 그 봇의 발신이 멈추고, 음성 기능 전체가 멈출 수 있어요. 20. 발신으로 받는 사람 정보(파발 id·허락했는지·받았는지·통화 시각과 길이·누가 끊었는지)는 선언한 목적에만 쓰고, 남에게 주지 않고, 허락이 꺼지거나 그 사람이 지워 달라고 하면 지워요. 받은 시각으로 사람의 일과나 있는 곳을 짐작하지 않아요. 21. 허락을 서비스의 조건으로 걸거나 속여서 받지 않아요(허락 링크 메시지 포함). 22. 전화마다 적는 목적은 그 전화의 실제 목적이에요. 깨우기 창을 다른 용도로 쓰지 않아요(서버는 목적을 봇이 적은 대로 믿어요). 23. 봇 토큰을 지키고, 샌 것을 알면 곧바로 @BotFather 에서 재발급해요. 토큰이 곧 사람에게 전화할 권한이에요. 24. 봇이 건 전화에는 대화 기록(1항)이 오지 않아요. 봇에게 가는 것은 20항의 것이에요.
약관에 동의하기

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.me でも無効です

運営者が 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)。

発信者 Pabal · ゲートウェイ オーナーの wss サーバー ① ストリーム確認画面 · 電話 ② アップグレード + 署名 · pabal.voice.v1 ③ start {call_id, bot_id, format} ④ ready(3 秒以内) AI の案内音声 · このあいだ発信者の音声は ボットのサーバーへ送られない ⑤ live 音声(中継) ⑥ PCM16 24 kHz · 20 ms 単位 ⑦ clear · mark · hangup ⑧ stop {reason} → 1000 で閉じる
ストリームモード:モデルの位置にボットのオーナーのサーバー

図の説明

  • 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"
  1. アドレスの形式:wss:// のみ、ユーザー名・#・空白なし、ASCII のみ(ほかの文字はパーセントエンコード)、2,048 文字以下。ホストはドットを含む名前か公的な IP(localhost は不可)、ポートは 443 または 1024〜65535。そうでなければ 400 STREAM_URL_INVALID: …。
  2. プローブの上限はリクエストした瞬間に数えます:ボットごとに 1 時間 5 回、オーナーごとに 10 回、同時にはボットごとに 1 つ・オーナーごとに 2 つ。超えると 429。ゲートウェイが混んでいてどこにも接続しなかったプローブは数えません。
  3. アドレスの確認:名前がプライベート・予約済みのアドレスや Pabal 自身のアドレスを 1 つでも指していれば、400 STREAM_URL_INVALID: the host must resolve only to public addresses。
  4. プローブ:ゲートウェイが 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 秒まで待ちます。
  5. 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、nonce 00112233445566778899aabbccddeeff、call 4611686018427387905、host bot.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.」(申し訳ありません。接続に問題が起きたため通話を終了します)を聞いて切れます。拒否はすべて話し中で終わります。

アプリの利用者向けの説明は、アプリの使い方 — 音声ボットに電話するにあります。

ボットからの電話

pabal.me では無効です — 法的な確認待ち

サーバーには実装されていますが、プライバシーポリシーの 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 つです。

流れ

ボットのコード Pabal サーバー ゲートウェイ · モデル 相手のアプリ ① startVoiceCall 判定:ボット · 許可 · 上限 着信しない時間帯 · 一時停止 ② 200 {voice_call_id} ③ dial ④ 着信:名前 (AI) ⑤ 応答 ⑥ Pabal の案内 ⑦ ボット:名前 · AI · 目的 通話 — 音声はゲートウェイと AI モデルを通る ⑧ voice_call: started · ended 不応答 · 着信なし:not_answered
ボットからの電話 1 本:依頼 → 判定 → 着信 → 応答 → 案内 → 最初のひと言 → 終了の通知

図の説明

  • 4 本のレーン:左からボットオーナーのコード、Pabal サーバー、音声ゲートウェイと AI モデル、着信者のアプリです。縦の点線が時間で、青い実線が進む流れ、灰色の線がボットへの答えです。
  • ①の灰色の枠 — 判定は 3 回あります:答える前にボット側(有効にしたか、運営者の審査、規約、上限)と許可があるかを、200 のあとに相手側(着信しない時間帯、一時停止中、相手の 1 日の上限と間隔、鳴らせる端末)を、鳴らす直前にもう一度確かめます。
  • ②の 200 は「受け付けた」という意味だけです。鳴ったか、出たかは⑧のアップデートでしかわかりません。
  • ④–⑦ 相手側:着信にはボット名のあとに「(AI)」が付き、出ると Pabal の案内音声(⑥)がボットより先に流れます。ボットは最初のひと言(⑦)で名前、AI であること、電話の目的を伝えます。緑の枠の間、音声はゲートウェイと AI モデルを通ります — エンドツーエンド暗号化ではありません。
  • 赤い点線が要点です:相手側の判定で鳴らなかった依頼と、鳴ったのに出なかった依頼は、同じ時刻に同じ not_answered で終わります。相手がそのとき寝ていたのか、別の通話中だったのか、ボットにはわかりません。

有効にする — @BotFather

ボットからの電話はボットオーナーが @BotFather でだけ有効にします。トークンではできません(setVoiceCallSettings に発信の項目を送ると 400 VOICE_FIELD_NOT_AVAILABLE)。先に音声通話が有効になっている必要があります。/mybots → ボット → 音声通話(음성 통화)→ ボットからの電話(봇이 거는 전화)→ オン(켜기)。

켜기
BotFather@order_helper_bot 이 사람에게 전화를 걸 수 있게 할까요? • 이 봇에게 전화를 허락한 사람에게만 걸어요. 허락은 그 사람이 파발 앱에서 하고, 언제든 끌 수 있어요. • 그 사람의 조용한 시간과 쉬는 중에는 울리지 않아요. • 하루 상한(지금 50번)과 서버의 한도가 있어요. • 운영자가 이 봇의 이름·사진·지시문·목소리·목적을 검토하고 허락해야 걸려요. 무엇이든 바뀌면 다시 검토해요. • 봇이 거는 전화는 종단 간 암호화가 아니에요. 통화 소리는 파발 서버와 AI 제공자를 거쳐요.
켜기
BotFather봇이 거는 전화를 켰어요. 운영자가 이 봇을 검토하고 허락하면 Bot API 의 startVoiceCall 로 걸 수 있어요. 봇이 거는 전화 · @order_helper_bot 상태: 켜짐 운영자 허락: 대기 목적: 배달 하루 상한: 50번 오늘: 요청 0 · 받음 0 · 안 받음 0 이 봇에게 전화를 허락한 사람에게만, 그 사람의 조용한 시간과 쉬는 중을 피해 걸어요. 봇이 거는 전화는 종단 간 암호화가 아니에요 — 통화 소리는 파발 서버와 AI 제공자를 거쳐요.

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_reason bot)。依頼から 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 日)には漏れなく残ります。
  • ボットオーナーに届くもの(許可画面と同じ順番):
    1. 相手の Pabal ID
    2. 許可がオンかどうか(電話の依頼への答えでわかる)
    3. 出たかどうか
    4. 通話の開始時刻と長さ
    5. 誰が切ったか
    出なかったのか拒否したのかは区別されません。会話の内容は届きません。
  • 共有の端末:ログインしている端末ならどこでも出られ、出たのが誰でもボットは目的と案内の内容を話します。何人かで使うパソコンならログアウトするよう伝えてください。
  • ロック画面: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 callsstartVoiceCall:選んでいない目的、韓国語・英語でないボットの言語
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_INVALIDgetVoiceCallUsage の期間
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_REQUIREDMCP サーバー(登録する)。503 VOICE_NOT_AVAILABLE: mcp: … は少し待ってから再試行、409 MCP_CHECK_IN_PROGRESS は検査が終わってから再試行
429 Too Many Requests: retry after N1 時間の上限(設定・キーのテスト・プローブ・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、それ以外は大文字のコードそのままです。