Developer docs
English

Bot development

Voice bots

When someone calls a bot from the app, an AI voice model answers. How to turn it on, instructions and voices, call updates, tools, MCP servers, own keys, transcripts, stream mode, the Apple Watch and keeping the conversation โ€” a guide for bot owners.

A voice bot is a bot that people can call from the app. The call is answered not by the bot owner's code but by Pabal's AI voice model; the bot owner uses the Bot API to set the instructions, greeting, voice, language and limits the model follows. You get a voice_call update each time a call starts and ends, and during a call the model can call the bot's code (tools) to look up an order or check a booking. There is also stream mode, in which your own server receives the raw audio instead of the AI model. If this is your first bot, read Build a bot first.

Current status

Voice bots must be turned on per server, and when this page was last updated (2026-10-02) they were off on pabal.me โ€” they are being tested on a development server

You can call a voice bot only on a server where all of the conditions below are met. The methods exist even on servers where voice is off, so you can read the settings. But until voice is turned on for this server and the operator has allowed the bot, no setting is saved (403 VOICE_NOT_AVAILABLE) except ones that turn something off: turning voice off, turning transcripts off, turning stream mode off, deleting your own key, deleting an MCP server. Whether your bot can take calls right now is shown by available and unavailable_reason in getVoiceCallSettings.

FeatureTo turn on (all required)pabal.me now
Voice-bot callsThe server has a voice gateway and a call relay (TURN) attached ยท the privacy policy's bot-call section (bot-calls) is published ยท the operator has allowed the bot ยท before public launch, only testers chosen by the operator can call ยท the caller's app is a build with the confirmation sheet (desktop pabal-bc2 โ€” while the confirmation sheet is only in Korean and English, pabal-bc1 is accepted too; Android 62151; iPhone build 5 or later; Apple Watch app build 1 or later) โ€” no such build has been released yetOff
Calling from the Apple WatchVoice-bot calls + the server's watch API + the watch audio edge (pabal-voice-edge) + a gateway that takes direct calls (direct) ยท a watch app build of TELEGRAM_VOICE_MIN_BUILD_WATCHOS or later ยท bots that use tools, MCP servers, an own key, stream mode or the bot owner's transcripts don't work yetOff โ€” being tested on a development server
Keeping the conversation (the caller's copy)Voice-bot calls + the policy's bot-calls-keep-text section + a gateway that announces keep_text (0.9.0) + an app build with the checkbox ยท legal review of the confirmation sheet's wordingOff โ€” awaiting legal review
Calls from botsThe privacy policy's bot-calls-outbound section and the terms of service's voice-bot-owners-outbound section are published (and the voice-bot-owners section, for owners to accept terms version 4) ยท the operator's global switch, after the legal review ยท the bot owner turns it on in @BotFather (terms version 4, purposes) ยท the operator's review of the bot ยท the callee's permission (an app build with the permission sheet) ยท a gateway with the approved outbound notice recordings ยท a bot language of Korean or EnglishOff โ€” waiting for the legal review
Bot languages other than Korean and English (7)Voice-bot calls + a gateway that takes calls in the bot's language (from 0.7.0 on it takes all nine) ยท the operator has read the bot's instructions and greeting and allowed it (even on a server that takes every bot)Off โ€” not yet deployed
Confirmation sheets and notices in languages other than Korean and English (8 languages)Each language ships in the app builds and the gateway image only after its legal review and a native speaker's check. Until then, callers who use the app in that language also see the English confirmation sheet and hear the English noticesDrafts โ€” not yet reviewed
Bot owner terms (version 4)The bot-owner section of the terms of service (voice-bot-owners) is published. Transcripts, own keys, stream mode, MCP servers and calls from bots all need these terms firstCannot be accepted
ToolsVoice-bot calls + a gateway that supports toolsOff
MCP serversThe MCP components ยท a dedicated egress address (an operating condition โ€” the code does not check it) ยท the policy's bot-calls-mcp section and the terms' bot-owner section ยท acceptance of the bot owner terms ยท an app build that shows the bot's MCP recipients on the confirmation sheet (until then, registering works but no call carries MCP tools)Off
Own model keyThe server's master key ยท the operator turns it on after the apps ship the confirmation sheet (version 2) that shows an own-key bot's recipient ยท the bot owner terms ยท a gateway that runs owners' keys (by default, Google keys only)Off โ€” keys are not even accepted
TranscriptsThe operator allows them on the admin page after a legal review (off by default) ยท the bot owner terms ยท the bot owner's switch ยท a gateway with the transcript notice audio ยท a managed callOff
Stream modeThe server's master key ยท the policy's bot-calls-stream section and the terms' bot-owner section ยท a gateway that takes stream calls ยท an app build with the stream confirmation sheet (none yet)Off
  • The caller (the person making the call) must be 19 or older, sees the "An AI answers this call" confirmation sheet before calling, and hears an AI notice first once connected (What callers see). Apps without the confirmation sheet (including the original Telegram apps and clients such as Telethon) have no call button for the bot.
  • The audio of a managed call goes to Google โ€” the Gemini Live API in Vertex AI, in Iowa, USA (Google Cloud us-central1). Pabal does not store audio. Google keeps neither audio nor text after producing its answer, but content flagged as possible abuse by automated checks may be logged for up to 90 days (the confirmation sheet says the same). This is the only engine in production, and calls are never quietly passed to another provider (no automatic failover).
  • Unlike calls between people, bot calls are not end-to-end encrypted: Pabal's voice gateway decrypts the audio and hands it to the AI model. The caller's confirmation sheet says so.

How a call flows

Caller Pabal (the operator's server) Outside Pabal Pabal app Confirm sheet ยท 19+ Patched build pabal-server MTProto ยท Bot API Admission ยท limits Call records Bot owner's program Bot API (HTTPS) Your code ยท your server coturn Relays audio only pabal-voice AI notice ยท instructions Stores no audio Google Vertex AI Gemini Live us-central1 (USA) โ‘  Call โ‘ก Link Calls ยท settings ยท tools โ‘ข voice_call ยท tool queries Settings ยท answers ยท endVoiceCall Audio (relay only) โ‘ฃ Audio โ†” AI replies
A managed call: signaling on top, audio below

About this diagram

  • Three areas: the caller's app โ†’ the Pabal operator's server (the Pabal server, the voice gateway, the relay server) โ†’ outside Pabal (Google's AI model, the bot owner's program). The bot owner's code never sees a single byte of audio.
  • โ‘  Call: the app reports the consent given on the confirmation sheet to the server, then places the call. The server checks both the caller (consent โ€” self-confirmed 19 or older ยท app build ยท tester ยท limits) and the bot (allowed ยท settings ยท limits), and either takes the call or ends it with a busy signal.
  • โ‘ก Link: the server hands the call to the voice gateway along with the bot owner's instructions, greeting, voice, language and tools. The gateway always puts Pabal's base rules (such as saying it is an AI) first and the instructions after them.
  • โ‘ข Bot API: the bot owner's program sets the settings, receives voice_call when a call starts and ends, and receives and answers voice_call_tool_query when the model calls a tool. The blue arrow at โ‘ข is what the server sends to the bot; the gray arrow below it is what the bot sends to the server.
  • โ‘ฃ Audio (bottom row): the app and the gateway exchange audio only through the relay server (coturn), without knowing each other's address, and the gateway passes the audio to Google's model and gets the replies back. The red box is where audio leaves Pabal.
  • In stream mode, the bot owner's server takes Google's place at โ‘ฃ (Stream mode).

Calls from the Apple Watch (coming soon)

The watch app doesn't use MTProto. The watch signs in, gets the bot list and the confirmation sheet, and places calls through the server's watch API (HTTPS JSON). The server runs the same admission checks as for an app call, then gives the watch a one-time ticket (15 seconds) usable only for that call, and hands the call to a gateway. The watch takes that ticket to the audio edge (WSS, pabal-voice-edge), and the edge connects the audio to the gateway worker handling that call. There is no key exchange (DH) and no call relay (coturn). The gateway still decodes the audio and hands it to the model, so the call is not end-to-end encrypted, and the audio goes to the same place. The bot owner gets the same voice_call as for an app call (only the moment of started differs, below).

Turning on voice calls

Voice calls are turned on by a person, in @BotFather โ€” agreeing to the voice-bot rules is the bot owner's promise, not the token's. Everything else can be set either in @BotFather or with the Bot API.

  1. Ask the operator to allow the bot. On a server that takes only allowed bots (the default), @BotFather has no voice-call button and changing settings gives 403 VOICE_NOT_AVAILABLE until the operator allows the bot by its bot ID (the number before the : in the token) in the admin page's voice tab. Before public launch, also ask for the account you'll make test calls from to be added as a tester (your user ID is message.from.id in getUpdates after you send the bot a message).
  2. Write the instructions. This is the text that says what the bot does on the phone. Set it with setVoiceCallSettings below, or send it with the Instructions (์ง€์‹œ๋ฌธ) button on @BotFather's voice-call screen. Without instructions it cannot be turned on (except in stream mode).
  3. In @BotFather: /mybots โ†’ the bot โ†’ Voice calls (์Œ์„ฑ ํ†ตํ™”) โ†’ Turn on (์ผœ๊ธฐ). The first time, the voice-bot rules appear; press Agree to the rules and turn on (๊ทœ์น™์— ๋™์˜ํ•˜๊ณ  ์ผœ๊ธฐ) to turn it on. After that it only asks "Turn it on?" (์ผค๊นŒ์š”?).
  4. Wait to be allowed again. On a server that takes only allowed bots, allowing a bot allows the instructions, greeting and tools as they were at that moment, so writing instructions after being allowed gives unavailable_reason: "awaiting_review". The bot takes calls again only after the operator reviews and allows it again. The same happens every time you change any of the three (in stream mode, the stream address). If the bot's language is neither Korean nor English, the operator must read the instructions and greeting and allow the bot before it takes calls, even on a server that takes every bot (Voices and languages).
  5. Check that available in getVoiceCallSettings is true, then try calling with the call button on the bot's profile in an app build with the confirmation sheet.
์ผœ๊ธฐ
BotFather์Œ์„ฑ ํ†ตํ™”๋ฅผ ์ผœ๊ธฐ ์ „์— ์Œ์„ฑ ๋ด‡ ๊ทœ์น™์„ ํ™•์ธํ•ด ์ฃผ์„ธ์š”. 1. ๋ด‡์€ ์–ธ์ œ๋‚˜ AI ๋ผ๊ณ  ๋ฐํ˜€์š”. ์‚ฌ๋žŒ์ธ ์ฒ™ํ•˜๊ฒŒ ํ•  ์ˆ˜ ์—†์–ด์š”. 2. ์‹ค์ œ ๊ธฐ๊ด€ยทํšŒ์‚ฌยท์‚ฌ๋žŒ์„ ์‚ฌ์นญํ•  ์ˆ˜ ์—†์–ด์š”. 3. ์นด๋“œ ๋ฒˆํ˜ธยท์ฃผ๋ฏผ๋“ฑ๋ก๋ฒˆํ˜ธยท๋น„๋ฐ€๋ฒˆํ˜ธยท์ธ์ฆ ๋ฒˆํ˜ธ๋ฅผ ๋ฌป๊ฒŒ ํ•  ์ˆ˜ ์—†์–ด์š”. 4. ํ†ตํ™” ์†Œ๋ฆฌ๋Š” ํŒŒ๋ฐœ์ด ์ €์žฅํ•˜์ง€ ์•Š๊ณ , ๋Œ€๋‹ต์„ ๋งŒ๋“ค๊ธฐ ์œ„ํ•ด Google(Vertex AI)๋กœ ๋ณด๋‚ด์ ธ์š”. 5. ๋ด‡๊ณผ์˜ ์Œ์„ฑ ํ†ตํ™”๋Š” ๋งŒ 19์„ธ ์ด์ƒ๋งŒ ํ•  ์ˆ˜ ์žˆ์–ด์š”. 6. ๊ทœ์น™์„ ์–ด๊ธฐ๋ฉด ์šด์˜์ž๊ฐ€ ์Œ์„ฑ ํ†ตํ™”๋‚˜ ๋ด‡์„ ๋ฉˆ์ถœ ์ˆ˜ ์žˆ์–ด์š”.
๊ทœ์น™์— ๋™์˜ํ•˜๊ณ  ์ผœ๊ธฐ
BotFather์Œ์„ฑ ํ†ตํ™”๋ฅผ ์ผฐ์–ด์š”. ์Œ์„ฑ ํ†ตํ™” ยท @order_helper_bot ์ƒํƒœ: ์ผœ์ง ๋ชฉ์†Œ๋ฆฌ: Kore (๋‹จ๋‹จํ•œ) ยท ์–ธ์–ด: ํ•œ๊ตญ์–ด โ€ฆ ์ „ํ™”๋ฅผ ๋ฐ›์œผ๋ฉด ๋จผ์ € "AI ์Œ์„ฑ ๋ด‡๊ณผ ์—ฐ๊ฒฐ๋˜์—ˆ์Šต๋‹ˆ๋‹ค" ์•ˆ๋‚ด๊ฐ€ ๋‚˜๊ฐ€๊ณ , ๊ทธ๋‹ค์Œ ๋ด‡์ด ์ธ์‚ฌํ•ด์š”.

BotFather replies in Korean. The rules above say: 1. the bot always says it is an AI and cannot be made to pretend to be a person; 2. it cannot impersonate a real organization, company or person; 3. it cannot be made to ask for card numbers, resident registration numbers, passwords or verification codes; 4. Pabal does not store call audio, which is sent to Google (Vertex AI) to produce the replies; 5. only people 19 or older can make voice calls with bots; 6. if the rules are broken, the operator can suspend voice calls or the bot. After you agree, BotFather confirms that voice calls are on and shows the bot's voice-call status (state: on, voice: Kore (firm), language: Korean), and notes that when a call is answered the notice "You're now talking to an AI voice bot." plays first and then the bot greets the caller.

On a server that takes only allowed bots, the status may read "์ผœ์ง โ€” ๋ฐ”๋€ ์ง€์‹œ๋ฌธยท์ธ์‚ฌ๋ง์„ ์šด์˜์ž๊ฐ€ ํ™•์ธํ•  ๋•Œ๊นŒ์ง€ ์ „ํ™”๋ฅผ ๋ฐ›์ง€ ์•Š์•„์š”" ("On โ€” calls are not taken until the operator reviews the changed instructions and greeting") (step 4 above). If the bot's language is neither Korean nor English, the status reads "์ผœ์ง โ€” ํ•œ๊ตญ์–ดยท์˜์–ด๊ฐ€ ์•„๋‹Œ ๋ด‡์€ ์šด์˜์ž๊ฐ€ ์ง€์‹œ๋ฌธยท์ธ์‚ฌ๋ง์„ ์ฝ๊ณ  ํ—ˆ์šฉํ•  ๋•Œ๊นŒ์ง€ ์ „ํ™”๋ฅผ ๋ฐ›์ง€ ์•Š์•„์š” (์ž๋™ ๊ทœ์น™ ๊ฒ€์‚ฌ๊ฐ€ ํ•œ๊ตญ์–ด์™€ ์˜์–ด๋งŒ ์ฝ์–ด์š”)" ("On โ€” a bot that is not in Korean or English takes no calls until the operator reads its instructions and greeting and allows it (the automatic rule check reads only Korean and English)") on every server until the operator allows the bot.

The voice-call screen has buttons for turning on and off, instructions, greeting, voice, language, limits, usage, transcripts, own key, stream mode and (on servers with MCP turned on) MCP servers. Turning off and deleting always work, even after voice is turned off or the bot is no longer allowed โ€” turning voice off, deleting your own key, turning stream mode off, deleting an MCP server.

The bot's name is also checked for impersonation when turning on and on every call. A bot that uses the name of a real organization or company cannot be turned on; you get VOICE_CONTENT_REFUSED: name: impersonation. If the bot really belongs to that organization, contact the server's support.

Settings โ€” setVoiceCallSettings

Sets the bot's voice settings. It is a partial update: only what you send changes; everything is checked first and then written at once, or all of it is rejected. If nothing changed, nothing is written.

curl -s "https://pabal.me/bot$BOT_TOKEN/setVoiceCallSettings" -H 'Content-Type: application/json' -d '{
  "instructions": "You are the phone agent of the Order Helper shop. Answer politely and briefly. Opening hours are 10 am to 8 pm.",
  "greeting": "Order Helper here. How can I help you?",
  "voice": "kore",
  "language": "ko-KR",
  "max_call_duration": 180
}'
# {"ok":true,"result":true}
ParameterTypeDescription
enabledBooleanTurn on or off. Turning on requires having agreed to the rules in @BotFather (otherwise 403 VOICE_RULES_NOT_ACCEPTED), and a managed bot must have instructions. Turning off always works โ€” calls in progress continue to the end; only new calls are refused
instructionsStringThe instructions the model follows. Up to 8,192 bytes of UTF-8 (about 2,700 Korean characters). "" clears them โ€” a managed bot that is on cannot have them empty. Longer instructions make calls cost more, and callers can find them out, so do not put passwords or keys in them
greetingStringThe first thing the bot says after the AI notice. Up to 512 bytes (about 170 Korean characters). With "" the model greets briefly on its own
voiceStringVoice id (list), default kore
languageStringThe language the bot speaks and understands: one of nine ids (default ko-KR; see the list). zh-CN is stored as cmn-CN, and an id that differs only in case (ja-jp) is stored as listed. If it is neither Korean nor English, the bot takes calls only after the operator has read its instructions and greeting and allowed it. The AI notice at the very start is not in this language but in the language of the confirmation sheet the caller saw
max_call_durationIntegerThe longest a call may last (seconds). At least 60 and at most limits.max_call_duration (the smaller of the operator's and the server's cap, default 600). Bot default 300
max_concurrent_callsIntegerNumber of calls taken at the same time. At least 1 and at most limits.max_concurrent_calls (default 2). Bot default 2
toolsArrayThe bot's tools that the model can call during a call (tools). [] removes them all
transcriptBooleanThe transcript switch (transcripts)
mode, stream_url, stream_secretStringStream mode (setting up stream mode)
model, api_keyNot accepted โ€” VOICE_FIELD_NOT_AVAILABLE. The server chooses the model, and keys are set only with setVoiceCallApiKey
  • Content rules: instructions, greetings and tool descriptions are checked when saved and on every call. Making the bot say it is a person (claims_human), making it hide that it is an AI (hides_ai), telling it to ignore Pabal's rules (overrides_rules), impersonating a real organization or company (impersonation), presenting it as an official body (official_claim), asking for card numbers, resident registration numbers, passwords or verification codes (asks_secrets), text that looks like a secret key (looks_like_secret) and invisible characters (hidden_text) are refused with VOICE_CONTENT_REFUSED: <field>: <code>. Negative sentences such as "Never ask for passwords" pass.
  • Pabal's base rules always come first. On every call the model reads Pabal's rules โ†’ the bot owner's instructions โ†’ the closing part of Pabal's rules, in that order, and the instructions are placed inside a block marked as "the bot owner's text". Instructions cannot replace or override Pabal's rules.
  • You can change the settings up to 30 times an hour (together with @BotFather; only actual changes count). Beyond that you get 429 Too Many Requests: retry after N and parameters.retry_after.
  • When sending as a form (-d name=value), put tools in as a JSON string.
  • A bot signed in only over MTProto sends the same JSON with bots.sendCustomRequest (MTProto bots).

Current settings โ€” getVoiceCallSettings

curl -s "https://pabal.me/bot$BOT_TOKEN/getVoiceCallSettings"
{
  "ok": true,
  "result": {
    "enabled": true,
    "available": true,
    "suspended": false,
    "rules_accepted": true,
    "instructions": "You are the phone agent of the Order Helper shop. โ€ฆ",
    "greeting": "Order Helper here. How can I help you?",
    "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
  }
}
FieldMeaning
enabledWhether the bot owner has turned it on
available, unavailable_reasonWhether the bot can take a call right now, whoever calls, and if not, why (table below)
rules_accepted, owner_terms_acceptedWhether the voice-bot rules and the current bot owner terms (version 4) have been accepted
limitsThe largest values you can set on this server and the daily limits (minutes)
voices, languagesThe voices and languages you can choose
testers_onlyTrue when voice bots are not yet public and only testers chosen by the operator can call
tools, tool_limits, tool_delivery, tool_delivery_problemTools and their limits, where tool queries would go right now (webhookยทget_updatesยทmtprotoยทnone) and why they cannot get there (Receiving tool queries)
mode, stream_url, stream_secret_fingerprint, has_stream_secret, stream_available, stream_ignoresStream mode โ€” the secret is never shown, only an 8-character fingerprint
transcript, transcript_available, transcript_unavailable_reason, transcript_delivery, transcript_delivery_problemThe transcript switch, and whether transcripts will actually be delivered now
has_api_key, api_key, own_keys_availableOwn model key โ€” the key is never shown, only the provider, fingerprint, date set and whether it can be used
mcp_available, mcp_servers, mcp_unavailable_reasonMCP servers
outboundThe state of calls from the bot (Calls from bots โ€” settings and usage)
updated_dateWhen the settings last changed (Unix seconds)
unavailable_reasonMeaning
voice_offVoice is off on this server, or the privacy policy's bot-call section is not there yet
kill_switchThe operator has stopped all voice calls
bot_voice_offThe bot owner has not turned it on
not_allowedThe operator has not allowed this bot
awaiting_reviewThe operator has not yet reviewed the changed instructions, greeting or tools (in stream mode, the stream address). If the bot's language is neither Korean nor English, the operator must read its instructions and greeting and allow it, even on a server that takes every bot
suspendedThe operator has suspended voice for this bot (or its owner's account)
name_refused, text_refusedThe bot's name, or the saved instructions, greeting or tool descriptions, do not meet the current content rules. text_refused also appears when the language the bot chose has been taken off the list and can no longer be used โ€” choose a language again
no_relay, no_gatewayThe server has no call relay or no voice gateway
no_gateway_with_tools, no_own_key_gatewayNo gateway can run this bot's tools or own key
no_gateway_with_languageNo connected gateway takes calls in this bot's language (Voices and languages)
own_key_unavailable, stream_unavailableThe own key or stream mode that was set cannot be used right now, so calls are not taken (they are not answered with the platform key instead)

Voices and languages

Choose from Gemini Live's 30 prebuilt voices (default kore). There is no way to upload or clone a voice. The Voice (๋ชฉ์†Œ๋ฆฌ) button in @BotFather lets you choose by name and character.

idCharacteridCharacteridCharacter
zephyrBrightpuckUpbeatcharonInformative
koreFirmfenrirExcitableledaYouthful
orusFirmaoedeBreezycallirrhoeEasy-going
autonoeBrightenceladusBreathyiapetusClear
umbrielEasy-goingalgiebaSmoothdespinaSmooth
erinomeClearalgenibGravellyrasalgethiInformative
laomedeiaUpbeatachernarSoftalnilamFirm
schedarEvengacruxMaturepulcherrimaForward
achirdFriendlyzubenelgenubiCasualvindemiatrixGentle
sadachbiaLivelysadaltagerKnowledgeablesulafatWarm

The language (language) is one of the nine below. It is the language the bot speaks and understands, and the bot answers in it. In @BotFather, choose it with the Language (์–ธ์–ด) button on the voice-call screen.

idLanguageGood to know
ko-KRKoreanDefault
en-USEnglishโ€”
ja-JPJapaneseโ€”
cmn-CNChinese (Mandarin)Outside Google's documented list of supported languages, so Google does not guarantee its quality (in our tests, both understanding and answers were accurate). zh-CN is also accepted and stored as cmn-CN
fr-FRFrenchโ€”
es-USSpanishโ€”
id-IDIndonesianโ€”
vi-VNVietnameseโ€”
ar-EGArabic (Modern Standard)The bot speaks Modern Standard Arabic. The code is ar-EG, the only Arabic code Google's real-time model has, but it does not set a dialect โ€” the bot speaks a dialect only when its instructions ask for one
  • A bot whose language is neither Korean nor English takes calls only after the operator has read its instructions and greeting and allowed it โ€” even on a server that takes every bot. This is because the automatic rule check at save time (pretending to be a person, impersonation and so on) reads only Korean and English. Until it is allowed, the reason given is unavailable_reason: "awaiting_review"; if you change the instructions, greeting or tools after it is allowed, it waits to be allowed again. @BotFather says so on the language picker and after you choose: "ํ•œ๊ตญ์–ดยท์˜์–ด๊ฐ€ ์•„๋‹Œ ์–ธ์–ด๋Š” ์ž๋™ ๊ทœ์น™ ๊ฒ€์‚ฌ๊ฐ€ ์ฝ์ง€ ๋ชปํ•ด์„œ, ์šด์˜์ž๊ฐ€ ์ง€์‹œ๋ฌธยท์ธ์‚ฌ๋ง์„ ์ฝ๊ณ  ํ—ˆ์šฉํ•œ ๋’ค์— ์ „ํ™”๋ฅผ ๋ฐ›์•„์š”." ("The automatic rule check cannot read languages other than Korean and English, so the bot takes calls only after the operator reads its instructions and greeting and allows it"). In the voice tab of the operator's admin page, the bot is shown as "ํ•œ๊ตญ์–ดยท์˜์–ด๊ฐ€ ์•„๋‹Œ ๋ด‡ (์ž๋™ ๊ทœ์น™ ๊ฒ€์‚ฌ๊ฐ€ ์ฝ์ง€ ๋ชปํ•จ): ์ง€์‹œ๋ฌธยท์ธ์‚ฌ๋ง์„ ์ฝ์–ด ์ฃผ์„ธ์š”" ("Not a Korean or English bot (the automatic rule check cannot read it): please read the instructions and greeting").
  • The bot takes calls only if there is a gateway that takes calls in the bot's language. When a gateway connects, it tells the server which bot languages it can take calls in (from 0.7.0 on it names all nine); a gateway that names none is assumed to run only Korean and English. If no gateway takes calls in the bot's language, the reason is no_gateway_with_language and the call button is hidden. The call is never run in another language instead.
  • Ids are accepted in any case (ja-jp) and stored as listed. A value that is not on the list is refused with 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, which names the accepted ids.
  • If the language you chose is later taken off the list, the bot keeps its settings but takes no calls (text_refused). It does not take calls in another language instead, so choose again under Language (์–ธ์–ด) in @BotFather.
  • Khmer cannot be chosen as a bot language yet. The model understands it, but no native Khmer speaker has checked its free-form answers yet.

If the caller keeps speaking another language, the model answers in that language. The AI notice is not in the bot's language but in the language of the confirmation sheet the caller saw (What callers see).

Receiving call updates โ€” voice_call

When someone calls the bot, a voice_call update arrives. The three voice updates (voice_call, voice_call_tool_query, voice_call_transcript) arrive only if you ask for them by name: put them in allowed_updates of getUpdates or setWebhook. An empty list does not bring them either (for the other kinds, an empty list means all of them).

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": "Minji", "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": "Minji", "language_code": "ko"},
    "date": 1790380952,
    "start_date": 1790380803,
    "duration": 149,
    "max_duration": 180,
    "end_reason": "caller"
  }
}
  • direction: "incoming" for a call to the bot, "outgoing" for a call the bot made (updates of calls from bots). Reading a missing field as a call to the bot, as before, is still right.
  • allowed_updates is one setting per bot, and the list last given to getUpdates or setWebhook is the one kept (if you leave it out, the previous value is kept). So if you give a list as in the example above, default kinds not in the list (edited_message, callback_query) no longer arrive โ€” list every kind your bot uses. Updates about calls that happened before you asked are not kept, so asking later does not bring them.
  • For the caller (from), only the ID, name and app language are sent. The username and phone number are not (the confirmation sheet promises this). A deleted account appears as first_name: "Deleted Account".
  • With a webhook, these arrive by POST in order like other updates. The getUpdates queue keeps up to the latest 1,000 per bot in memory and is lost when the server restarts.
  • Nothing is sent to a suspended or deleted bot.
eventWhenExtra fields
startedWhen the caller's app has confirmed the call (key exchange done) and the server has told the gateway the call is on. The server does not see the audio, so this is "on" as far as the server knows. For an Apple Watch call there is no key exchange, so it is when the gateway reports that the watch's audio has connectedmax_duration
endedWhen a call that was on has ended. If it waits for the transcript, it arrives up to 10 seconds late. On a server with conversation keeping on, every call that isn't in stream mode arrives up to 10 seconds late whatever the caller chose โ€” so that the bot owner can't tell what the caller chosestart_date, duration (seconds it was on, by the server's clock), max_duration, end_reason, (if transcripts were on) transcript
missedA call that ended before it was on (hung up while ringing, etc.)end_reason
refusedOnly when the caller passed every check but the call could not be taken because of the bot's side (concurrent calls, daily minutes, gateways); other refusals are not reported to the bot ownerrefused_reason: busy (concurrent calls full) ยท budget (the bot's, owner's or server's daily minutes are used up) ยท unavailable (the gateways that could take it are full)

end_reason: caller (the caller hung up) ยท bot (the bot hung up, endVoiceCall) ยท max_length (maximum length) ยท failed (connection failed) ยท operator (the operator or the kill switch) ยท suspended (the bot was suspended or deleted).

For every call handed to a gateway, a call record (the call's length or "Cancelled call") is left in the chat between the caller and the bot (calls refused before being taken leave none). The bot's copy does not arrive as a message update โ€” the bot's record is voice_call. If the caller chooses to keep the conversation, the conversation text is left only in the caller's chat, right below the call record, and nothing is left on the bot's side. On a server with conversation keeping on, 30 message numbers after the call record are reserved in advance, so the number of the message after a call record is at least 31 more than the record's (the same for calls whose conversation isn't kept).

During a call

Hanging up โ€” endVoiceCall

curl -s "https://pabal.me/bot$BOT_TOKEN/endVoiceCall" -d voice_call_id=4611686018427387905
  • farewell (Boolean, default true): if the call is on, the model says a short goodbye and hangs up. If it has not hung up within 10 seconds, the server ends the call. With false it hangs up at once. Stream-mode calls always hang up at once.
  • The ended call's voice_call.end_reason is bot. If it is not a live call of this bot, you get 400 VOICE_CALL_NOT_FOUND.

Giving the model a note โ€” sendVoiceCallContext

Tells the model something it should know during a call. The model reads it as the bot owner's text, not as the caller's words, and Pabal's rules still come first.

curl -s "https://pabal.me/bot$BOT_TOKEN/sendVoiceCallContext" -d voice_call_id=4611686018427387905 \
  --data-urlencode "text=This caller has just cancelled order A-1042. Tell them so if they ask." -d respond=false
  • text: up to 2,000 characters (6,144 bytes), checked against the content rules. With respond=true the model reacts right away; with the default, false, it uses the note on its next turn.
  • Notes also go to the model (Google, for a managed call), and the model may say them to the caller โ€” put in only what the caller may know.
  • Before the call is connected: 400 VOICE_CALL_NOT_ACTIVE; for a stream call or a gateway that does not take notes: 400 VOICE_CONTEXT_NOT_SUPPORTED; more than 10 per minute per call: 429 (retry_after 60).

Usage โ€” 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 (default) or month. With month you can choose the month, as in month=2026-09 (a future month gives VOICE_USAGE_PERIOD_INVALID).
  • Day and month boundaries follow the server's time zone (default Asia/Seoul). remaining_seconds_today is the daily limit minus ended calls and the time reserved by calls in progress.
  • Per-call usage records (without the caller) are kept for 180 days after the month ends; monthly totals stay as long as the bot exists.

Tools โ€” calling the bot's code during a call

When a caller asks "Where is my order A-1042?", the model calls the bot's check_order tool, the server passes the query to the bot, and the model tells the caller the bot's answer. The bot owner defines the tools, and the bot's code answers them.

Model ยท voice gateway Pabal server The bot's code โ‘  check_order {"order_id": "A-1042"} Checks args, drops extras Card/ID numbers stop here โ‘ก voice_call_tool_query ยท budget_ms Quiet: "One moment, let me check." 5 s: "Just a moment longer, please." โ‘ข answerVoiceCallToolQuery(result) Checked: no secrets or cards Up to 16,384 bytes โ‘ฃ tool_result (as data only) The model says the result Late: "The tool did not answer in time."
One tool query: model โ†’ server โ†’ bot โ†’ server โ†’ model

About this diagram

  • Three lanes: on the left the model and the voice gateway, in the middle the Pabal server, on the right the bot owner's code. The vertical dotted lines are time. The model never talks to the bot directly; every query and answer passes through the server.
  • The gray box at โ‘  โ€” the server checks the arguments first: whether they match the tool's parameters, their size and depth, undeclared keys (dropped before passing on), and values that look like card numbers or resident registration numbers (these do not reach the bot, and the model hears a refusal).
  • What the bot receives at โ‘ก is the query id, the call id, the caller, the tool name, the arguments (a JSON string) and budget_ms โ€” the milliseconds left until the answer must reach the server. While waiting, the gateway fills the silence (the box on the left).
  • The answer at โ‘ข is checked too: anything that looks like a secret, words that would make the model break the rules, and card or resident registration numbers are refused, and the model hears an error. The first answer is final, so even a refused answer closes the query.
  • At โ‘ฃ the solid blue line is the normal case and the red dashed line is a missed deadline. The model receives the tool's result only as data, never as instructions; on failure it hears one of Pabal's short phrases and explains the situation to the caller.

Defining tools

curl -s "https://pabal.me/bot$BOT_TOKEN/setVoiceCallSettings" -H 'Content-Type: application/json' -d '{
  "instructions": "You are the phone agent of the Order Helper shop. Answer politely and briefly. When the caller says an order number, look up the delivery status with the check_order tool and tell them.",
  "tools": [{
    "name": "check_order",
    "description": "Looks up the delivery status by order number. Call it when the caller says an order number.",
    "parameters": {"type": "object",
                   "properties": {"order_id": {"type": "string", "description": "Order number, for example A-1042"}},
                   "required": ["order_id"]},
    "timeout_ms": 5000
  }]
}'
ItemRule
KeysOnly these four: name, description, parameters (optional), timeout_ms (optional)
Number and size16 in total, counting the bot's tools and pinned MCP tools together; the definitions (nameยทdescriptionยทparameters) add up to at most 8,192 bytes of UTF-8. The same for own-key bots
name[A-Za-z_][A-Za-z0-9_-]{0,63}. It cannot contain __ (used in MCP tool names), cannot start with pabal, and names must not clash, ignoring case
descriptionRequired, up to 1,024 bytes, checked against the content rules. The model decides when to call the tool from this, so be specific
parametersThe top level is {"type": "object"}. Leaving it out means "no arguments"
timeout_ms1,000โ€“15,000, default 5,000. The longest the model waits for the answer

parameters uses only the part of JSON Schema that both engines accept.

  • Only ten keywords: type, description, enum, properties, required, items, minimum, maximum, minItems, maxItems. type is a single one (stringยทnumberยทintegerยทbooleanยทarrayยทobject).
  • propertiesยทrequired only on objects, itemsยทminItemsยทmaxItems on arrays (items required), minimumยทmaximum on numbers, enum only on strings (1โ€“32 values, 64 bytes each).
  • Property names are [A-Za-z_][A-Za-z0-9_]{0,63}; up to 24 properties per tool; objects up to 3 levels deep and objects and arrays together up to 5. Parameter descriptions up to 512 bytes.
  • Parameters that ask for secrets are refused: if the name contains a word such as pinยทcvvยทpasswordยทotpยทssnยทrrnยทcardnumber, or the description mentions card numbers, passwords, verification codes or resident registration numbers, you get asks_secrets.
  • Errors report the first problem with its path: VOICE_TOOL_INVALID: tools[0].parameters.properties.tags: items_missing, VOICE_TOOL_INVALID: tools[0].name: reserved, VOICE_CONTENT_REFUSED: tools[0].description: impersonation.
  • Tools are defined only with the Bot API. @BotFather's voice-call screen only shows the tool names.
  • On a server that takes only allowed bots, adding or changing tools means no calls are taken until the operator allows the bot again (awaiting_review, step 4 of turning on).

Receiving tool queries

{
  "update_id": 813,
  "voice_call_tool_query": {
    "id": "8190448532776044545",
    "voice_call_id": "4611686018427387905",
    "from": {"id": 100002, "is_bot": false, "first_name": "Minji", "language_code": "ko"},
    "name": "check_order",
    "arguments": "{\"order_id\":\"A-1042\"}",
    "budget_ms": 4700
  }
}
  • arguments is the text of a JSON object (at most 4,096 bytes and 8 levels deep; the server has already checked it). Even so, these are values the model made from what the caller said, so have the bot check them again. Knowing an order number must not be enough to hear someone else's order โ€” tell only about orders belonging to from.id (the caller as verified by Pabal).
  • budget_ms is the number of milliseconds from when the bot receives the query until the answer must reach the server. It is the tool's timeout_ms minus the way back to the model (200 ms) and the way to the bot (100 ms), so it is usually about 300 ms less than timeout_ms. It does not depend on the two sides' clocks agreeing.

The way a query reaches the bot is decided in this order (getVoiceCallSettings.tool_delivery).

  1. If there is a webhook, through the webhook. The webhook's allowed_updates must include voice_call_tool_query; if it does not, the query is not sent any other way and never arrives (tool_delivery_problem: "not_in_allowed_updates"). Queries are separate POSTs, so they are not held up in the ordered queue, and they are not sent again.
  2. If there is no webhook and the bot has called getUpdates within the last 15 seconds, at the front of the next getUpdates. Here too it must be in allowed_updates. Up to 8 waiting queries per bot.
  3. Otherwise, if the bot is connected over MTProto, as updateBotWebhookJSONQuery (regardless of allowed_updates).
  4. If none of these, it does not arrive (tool_delivery_problem: "not_listening"), and the model hears "The tool could not be reached."

Answering โ€” answerVoiceCallToolQuery

ParameterDescription
tool_query_idThe update's voice_call_tool_query.id
resultThe result. Text, or an object or array if the body is JSON (text that reads as a JSON object or array is also treated as JSON). Up to 16,384 bytes of UTF-8 (the operator may set it lower)
errorThe bot's own failure message, up to 512 bytes. The model passes these words on to the caller
  • Exactly one of result and error (neither gives TOOL_RESULT_EMPTY, both gives TOOL_RESULT_INVALID). Returns True.
  • The tool's answer goes to the model (Google, for a managed call), and the model tells the caller what it says โ€” put in only what the caller may know.
  • The first answer is final. Late or finished queries get 400 TOOL_QUERY_EXPIRED: <why> โ€” answered (already answered), timeout, cancelled (the caller barged in and the model withdrew it), call_ended, unreachable. An unknown id or one older than 10 minutes gives TOOL_QUERY_ID_INVALID.
  • Answers are checked too: invisible characters are removed, and anything that looks like a secret (looks_like_secret), words that would make the model break the rules (overrides_rulesยทhides_aiยทclaims_humanยทasks_secrets) and card or resident registration numbers (sensitive_data) give TOOL_RESULT_REFUSED: <code>. The bot owner's own data, such as the shop's address, phone number or URL, passes. Going over the size gives TOOL_RESULT_TOO_LONGยทTOOL_ERROR_TOO_LONG.
  • With a webhook you can answer in the response body โ€” put this in the 200 response to the tool-query POST (no other method is run). Always check the X-Telegram-Bot-Api-Secret-Token header before answering. If you put customer data in the response without checking, anyone who sends a fake query to that address gets the answer.
{"method": "answerVoiceCallToolQuery", "tool_query_id": "8190448532776044545",
 "result": {"order_id": "A-1042", "status": "In transit", "eta": "Arrives tomorrow afternoon"}}

Example โ€” python-telegram-bot

A bot that answers the check_order tool above. It receives with getUpdates and handles queries at the same time as other updates (concurrent_updates) โ€” if it is slowed down handling earlier updates, budget_ms runs out. Orders are looked up by the Pabal user ID of the person who placed them, and an order is revealed only when that ID matches the query's from.id.

# order_voice_bot.py โ€” pip install "python-telegram-bot==22.*"
# Run: read -rs BOT_TOKEN && export BOT_TOKEN
#      CUSTOMER_ID=<the Pabal user ID of the person who ordered> python3 order_voice_bot.py
# For testing, use your own ID as CUSTOMER_ID โ€” it is message.from.id in getUpdates after you message the bot
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 = {                                    # a small table for the example: really, the shop's order system
    (CUSTOMER, "A-1042"): {"status": "In transit", "eta": "Arrives tomorrow afternoon"},
}


def check_order(arguments, caller_id):
    """{"result": โ€ฆ} or {"error": โ€ฆ}, to pass straight to answerVoiceCallToolQuery"""
    order_id = str(json.loads(arguments).get("order_id", "")).strip().upper()
    if not re.fullmatch(r"[A-Z]-\d{4}", order_id):     # the model made the arguments โ€” check them again
        return {"error": "I did not catch the order number. For example: A-1042"}
    found = ORDERS.get((caller_id, order_id))          # only the caller's own orders
    if found is None:                                  # someone else's order gets the same words as a missing one
        return {"error": f"I could not find order {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")  # kinds PTB does not know end up in api_kwargs
    if q is None:
        return
    if q["name"] == "check_order":
        answer = check_order(q["arguments"], q["from"]["id"])
    else:
        answer = {"error": "I do not know that tool."}
    try:
        await context.bot.do_api_request("answerVoiceCallToolQuery",
                                         api_kwargs={"tool_query_id": q["id"], **answer})
    except TelegramError as e:                         # TOOL_QUERY_EXPIRED: too late, or the call ended
        print("The answer was not accepted:", 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 skips update kinds it does not know, so you have to pick up voice_call_tool_query from update.model_extra first, in the dispatcher's outer middleware (dp.update.outer_middleware). There is an example in the Pabal repository (scripts/bots/voice_tools_aiogram.py); ask the server's operator for it if you need it.

What the model and the caller hear

  • Success: the result becomes the model's function response (a JSON value if it is JSON). Text inside the result is read only as data and never becomes instructions.
  • Failure: the model hears one of Pabal's short phrases in the call's language and explains the situation to the caller โ€” for a missed deadline, "The tool did not answer in time."; when the tool could not be reached, "The tool could not be reached."; for arguments such as card numbers, "Pabal does not pass on card numbers or national ID numbers."; and for the bot's error, its exact words. For wrong arguments the model gets a hint about what was wrong, so it can call again.
  • While waiting, the gateway fills the silence: after 1.5 seconds of quiet it says "One moment, let me check." once (right away if the tool has recently been slow), and after 5 seconds "Just a moment longer, please." once. These two phrases are in the bot's language, not the caller's, and in English when there is no reviewed recording in that language (today only Korean and English are reviewed).
  • If the caller cuts in (barge-in), the gateway may withdraw open queries, and answers after that get TOOL_QUERY_EXPIRED: cancelled.

Tool limits

At most 2 queries can be open at once per call, and 8 per bot. The model can call tools at most 30 times a minute per call and 120 times a minute per bot. Beyond that, nothing is sent to the bot, and the model immediately hears too_many_open or rate_limited.

Bot owner terms

Transcripts, own model keys, stream mode, MCP servers and calls from bots can be turned on only after agreeing to the bot owner terms (version 4). A person agrees on @BotFather's voice-call screen (from any of the transcripts, own key, stream mode, MCP server or calls from bots screens) โ€” it cannot be done with the token. Even if you agreed to an earlier version, you are asked again. The agree button does not appear until the bot-owner section is published in the server's terms of service (requests before then get 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 shows the terms in Korean; the button reads "Agree to the terms". In short: 1. with transcripts on, you receive the text of the conversation written down by the AI when a call ends, and you are responsible for handling and protecting it; 2. callers to a bot with transcripts on are told first, by a spoken notice when the call connects, that the text goes to the bot owner; 3. keep transcripts only as long as needed, delete them when the caller asks, and do not use them to train models; 4. with your own model key, the call's audio and text go to your key's provider under your contract, you are responsible for that processing and say so in your privacy notice; 5. in stream mode, call audio goes to your server instead of Pabal's AI model, you are the one who receives and processes it, you use the caller's voice and words only as far as the law allows and delete them when no longer needed; 6. if a caller asks where your server is and what it does, you tell them (ideally in the bot's description); 7. the bot always says it is an AI and its replies are made by an AI โ€” no person answers directly and no human recording is played as if it were an AI reply; in stream mode Pabal plays the notice "This is an AI voice bot. The call's audio goes to the bot developer's server." at the very start of the call; 8. you never ask for card numbers, resident registration numbers, passwords or verification codes, and you keep the voice-bot rules; 9. Pabal does not store transcripts โ€” once delivered they are deleted from the server within 24 hours at the latest, and keys and the secrets of stream mode and MCP servers are kept encrypted and never logged; 10. if you register an MCP server, values the caller said go to that server when the model calls its tools during a call, you are responsible for the processing by that server and wherever it passes data on, and you declare the recipients truthfully; 11. if the terms are broken, the operator can suspend voice calls or the bot.

Items 12โ€“24 are what you keep when you turn on calls from bots: use them only for the purposes you declared and the operator allowed, with no advertising; say the bot's name and that it is an AI in its first words; put no sensitive information in context; never try to call anyone under 19; never work around quiet hours, pauses or limits; use what you learn about the people called only for those purposes and delete it when the permission is turned off; keep the token safe. Calls a bot makes bring no transcript (item 24). The wording of the terms may change after the legal review.

MCP servers

MCP servers are off by default, and off on pabal.me too

They turn on only when the operator has the MCP components (pabal-mcp, the signer, the egress proxy) in place and has published the privacy policy's bot-calls-mcp section and the terms of service's bot-owner section (giving MCP requests a dedicated egress address of their own is an operating condition, and the code does not check it). Even once on, until the apps show the bot's MCP recipients on the confirmation sheet, registering works but no call carries MCP tools (mcp_unavailable_reason: "sheet"). On a server with MCP off, setVoiceCallMcpServer gives 403 VOICE_NOT_AVAILABLE: MCP tools are not available on this server, and @BotFather has no "MCP servers" (MCP ์„œ๋ฒ„) button.

If the bot owner registers an MCP server that they run (or have a contract for), the model can call that server's tools during a call. It is Pabal, not the bot's code, that calls them โ€” Pabal's MCP client sends the request to that server through Pabal's egress proxy and enforces deadlines, sizes and checks. So values the caller said go to that server (bot owner terms, item 10).

What the MCP server must provide

  • Streamable HTTP, POST (protocol version 2024-11-05 or later). https:// with a public certificate, no redirects, and only ports the server allows (443 on pabal.me).
  • A server that uses credentials must honestly refuse with 401 without credentials and must not accept wrong credentials. One credential can be used by only one bot.
  • Operator document: the server's operator puts a JSON document (32 KB or less, no redirect) at https://<host>/.well-known/pabal-voice-mcp โ€” who runs it, its country and contact, its purpose, the kinds of data and where they are, recipients and sub-processors, and how long data is kept. This is the recipient information shown on the caller's confirmation sheet; if there is none, the bot owner declares it when registering (below). A public server anyone can call without credentials (auth.type: "none") must state in this document's endpoint entry "accepts": "any", saying it accepts calls from Pabal.
  • What Pabal adds: the _meta of tools/call carries the remaining deadline me.pabal/budget_ms (milliseconds) and the result-size limit me.pabal/result_budget_bytes, and may carry an assertion signed by Pabal (me.pabal/assertion, a JWT). The assertion's issuer, sub (bot:<bot id>) and verification keys (JWKS) are given by getVoiceCallMcpBinding.
  • tools/list at most 2 MB and 20 pages. There must be no steps a person has to go through (such as a browser sign-in).
  • Each tool gets a classification in _meta["me.pabal/voice"] โ€” data (noneยทcallerยทthird_party), content (systemยทthird_party_text), effect (noneยทwriteยทnotifyยทfinancial), max_ms (at most 15,000), max_result_bytes (at most 65,536), and optionally p95_ms. If the server does not give these, the bot owner supplies them in tools when registering.
  • Profile 1.0 grants only L1: only tools with data: "none" and effect: "none" (tools that only read public information) are pinned to calls. Tools that handle the caller's data (L2) or carry something out (L3) are recorded but not used.
  • When you register, Pabal runs conformance checks (address, TLS, version, 401, tool list, tool structure, credential scope, human steps). If it fails, the registration is refused with MCP_CONFORMANCE_FAILED: CT-xx.

Registering

Secrets are sent only in the request body (in a query string: 400). Keep them in a file and send that, so the secret is not left in your command line.

umask 077
cat > mcp-shop.json <<'EOF'
{"label": "shop",
 "url": "https://mcp.example.com/mcp",
 "auth": {"type": "bearer", "token": "(the token the MCP server gave you)"}}
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}, cannot start with pabal. 2 servers per bot. The tool names the model sees are label__toolname.
  • auth.type: none, bearer (token), headers (up to 3 headers), client_credentials (token_url, client_id, client_secret, or private_key_jwt: true โ€” call createVoiceCallMcpClientKey first). authorization_code is not available yet. none (a public server) is registered only if its operator's public document says it accepts calls from Pabal (otherwise MCP_OPT_IN_REQUIRED).
  • Changes that change the recipient (a new server, a different address or token host, a new declaration, a higher level) are answered with 400 MCP_CHANGE_NEEDS_CONFIRMATION, and take effect only when the bot owner, within 5 minutes, presses the Confirm (ํ™•์ธ โ€” ์ด ๋ณ€๊ฒฝ์„ ์ ์šฉ) button that appears in @BotFather. This is so that even if the token leaks, callers' words cannot be quietly sent somewhere else. Rotations that change only the secret take effect at once.
  • If the server's operator has no public document, the bot owner truthfully declares the recipient (who runs it, country, contact, purpose, kinds of data, location, sub-processors, how long data is kept) in declaration. This is shown on the caller's confirmation sheet, and when it changes, callers are asked again.
  • Registrations and checks: 10 an hour per bot, 20 per owner. Other methods: refreshVoiceCallMcpServer (check again), deleteVoiceCallMcpServer (always works), getVoiceCallMcpBinding (the issuer, sub and JWKS your server uses to recognize calls from Pabal), createVoiceCallMcpClientKey.
  • The state of the registered servers and tools appears in getVoiceCallSettings.mcp_servers (paths, queries and secrets are not shown). The full profile (Pabal Voice MCP Profile 1.0) is in the Pabal repository at docs/app/voice-bot-mcp-profile.html; ask the server's operator for it if you need it.

Own model key

Own keys cannot yet be used on any production server

For a call answered with an own key, the caller must see a confirmation sheet (version 2) saying that the audio goes to that key's provider, and no app shows that sheet yet. So servers other than test servers do not even accept keys until the operator turns them on once the apps are ready (403 VOICE_OWN_KEY_NOT_AVAILABLE). The server also needs a master key.

Runs the bot's calls on the bot owner's own model account. Charges go to the owner's provider account, and own-key calls do not use the platform's bot, owner or server daily limits (the caller's limits still apply). The bot owner terms come first.

curl -s "https://pabal.me/bot$BOT_TOKEN/setVoiceCallApiKey" -d provider=google \
  --data-urlencode "api_key@vertex-sa.json"
  • provider: google is a Vertex AI service account JSON key (only the needed fields are kept), openai is an API key starting with sk-. Anything else gives VOICE_PROVIDER_INVALID. The key goes only in the request body (in the query: 400) โ€” reading it from a file as above keeps it out of your command line. Do not paste it into the @BotFather chat.
  • Before accepting the key, the server checks that it really opens, with a test session on the gateway (up to 12 seconds). If it passes, the key is kept and you get True. If the provider refuses it: API_KEY_INVALID; if it signs in but has no access to the realtime API: API_KEY_NO_REALTIME_ACCESS; if there is no gateway to test with, or it is busy: 503 API_KEY_CHECK_UNAVAILABLE: โ€ฆ: the key was not kept.
  • Tests: 5 an hour per bot, 10 per owner (all bots together). Only tests that reached a gateway count.
  • The key is never shown anywhere โ€” not in answers, errors, logs or screens. getVoiceCallSettings.api_key has only the provider, the date it was set, a fingerprint (8 characters) and whether it can be used.
  • If the key that was set cannot be used (the terms changed, the server cannot open the key, and so on), the bot does not take calls โ€” it does not quietly switch to the platform key. Deleting the key with deleteVoiceCallApiKey (always works) makes the bot take calls with the platform key, and calls in progress on that key end.
  • Own-key calls have no Pabal daily limit โ€” only the call length, concurrent calls and the caller's limits apply. The charges go to your account, so set budget alerts in the provider's console.
  • OpenAI keys are used only if the gateway's operator turns them on separately (by default gateways take Google keys only). If no gateway has them on, testing an OpenAI key gives 503 โ€ฆ no_gateway. The OpenAI key path has not yet been tested against the real OpenAI (only against fake servers), and calls answered with an OpenAI key use the OpenAI voice chosen by the gateway (default marin), whatever the bot's voice setting. Google keys are used in the gateway's Vertex region (us-central1).
  • In stream mode, the key is only kept, not used.

Transcripts

Transcripts are delivered only on servers where the operator has allowed them

The operator must finish a legal review and allow them on the admin page (off by default). On a server that has not allowed them, turning the switch on gives transcript_available: false, transcript_unavailable_reason: "not_allowed", and no transcripts arrive.

When turned on, the bot owner receives the text of the call's conversation as written down by the AI. Turn the switch on after agreeing to the bot owner terms.

Transcripts and keeping the conversation are different

Transcripts (this section) are turned on by the bot owner and received by the bot owner, and the caller hears a notice when the call connects. Keeping the conversation is chosen by the caller with a checkbox on the confirmation sheet, and after the call the text is left only in that person's own chat โ€” it does not go to the bot or the bot owner, and there is no spoken notice. One call can have both. There is no switch for the bot owner to turn conversation keeping on; it is on when the server's privacy policy has the bot-calls-keep-text section (coming soon โ€” Using the app โ€” Voice bots).

curl -s "https://pabal.me/bot$BOT_TOKEN/setVoiceCallSettings" -d transcript=true
  • Calls a bot makes have no transcript (calls from bots) โ€” even with the switch on.
  • People who call a bot with transcripts on hear "What is said on this call is written down and sent to the bot's owner." right after the AI notice. Only what is said after the notice is written down. Stream-mode calls have no transcripts.
  • The voice_call of an ended call (event: "ended") carries transcript. This is the only complete copy, so keep voice_call in allowed_updates. To receive turns one at a time during the call, add voice_call_transcript too (HTTP Bot API only; it does not arrive over MTProto).
  • Turns containing a card number or resident registration number are delivered with every digit replaced by *. Numbers spread over two turns or read out as words are not caught, so handle the text you receive with care as well.
  • Up to 4,096 bytes per turn, 256 KiB and 4,000 turns per call. Anything beyond that is dropped, and you get truncated: true.
  • Pabal does not store transcripts: they are in memory during the call, handed to the bot once, and whatever is not picked up is deleted after 24 hours. They are lost if the server restarts. Keeping and processing them after they are received is the bot owner's responsibility.
"transcript": {
  "turns": [
    {"role": "bot", "text": "Order Helper here. How can I help you?", "offset_ms": 5200},
    {"role": "caller", "text": "Where is my order A-1042?", "offset_ms": 9800}
  ],
  "truncated": false
}
{"update_id": 21, "voice_call_transcript": {"voice_call_id": "4611686018427387905", "seq": 2,
 "role": "caller", "text": "Where is my order A-1042?", "is_final": true, "offset_ms": 9800, "date": 1790380813}}

role is caller (the caller) or bot (the model), and offset_ms is the milliseconds since the call's audio started.

Stream mode

Stream mode cannot be used yet

People can call a stream-mode bot only if their app shows the stream confirmation sheet, saying that "the call's audio goes to the bot developer's server", and no app build with that sheet exists yet. The server also needs a master key, the privacy policy's bot-calls-stream section, the bot-owner section and a gateway that takes stream calls. Without them, mode=stream gives 403 VOICE_STREAM_NOT_AVAILABLE.

In stream mode, the call's raw audio goes not to Pabal's AI model but to the bot owner's server. Your server listens to the audio and sends its reply back as audio โ€” you decide which AI to use (another provider's realtime model, your own combination of speech recognition and synthesis). Pabal still plays the AI notice at the very start and keeps enforcing the caller's checks, limits, call length and concurrent-call limits. No instructions are needed, and tools, own keys and transcripts are only kept, not used (stream_ignores).

Caller Pabal (server ยท gateway) Bot owner's wss server โ‘  Stream confirm sheet ยท call โ‘ก Upgrade + signature ยท pabal.voice.v1 โ‘ข start {call_id, bot_id, format} โ‘ฃ ready (within 3 s) AI notice plays ยท meanwhile the caller's audio does not go to the bot server โ‘ค live Audio (relay) โ‘ฅ PCM16 24 kHz ยท 20 ms chunks โ‘ฆ clear ยท mark ยท hangup โ‘ง stop {reason} โ†’ close 1000
Stream mode: the bot owner's server in the model's place

About this diagram

  • Three lanes: on the left the caller, in the middle Pabal (the server and the voice gateway), on the right the bot owner's server. The bot server talks neither to the app nor to the Pabal server directly, but only to the gateway, over a WebSocket.
  • โ‘กโ€“โ‘ฃ happen while the phone is ringing: the gateway connects over TLS to the public address it has checked, upgrades with the signature header and sends start. The call is taken only if the bot server sends ready within 3 seconds; otherwise the caller hears a busy signal.
  • The gray box is a gate: until the AI notice ("This is an AI voice bot. The call's audio goes to the bot developer's server.") has finished playing, the caller's audio does not go to the bot server, and audio the bot sends early is not played. The moment it ends, โ‘ค live is sent.
  • โ‘ฅ Audio in both directions is PCM16, 24 kHz, mono; on the caller's side it is always 20 ms (960-byte) chunks. Blue arrows are what Pabal sends; gray is what the bot server sends.
  • โ‘ฆ and โ‘ง: the bot can send barge-in (clear), playback-position notices (mark) and hang-up (hangup); when the call ends, Pabal sends a stop with the reason and closes with 1000.

Setup and the probe

First, run the example server below on a public address (a name its certificate matches). The probe connects to that server the moment you set it up, and failures that could not reach it also count toward the limit of 5 an hour.

After agreeing to the bot owner terms (@BotFather โ†’ Voice calls โ†’ Stream mode โ†’ Agree to the bot owner terms (๋ด‡ ์ฃผ์ธ ์•ฝ๊ด€์— ๋™์˜)), tell Pabal the server's address and secret. The secret is 32โ€“128 printable ASCII characters (no spaces), and a new address always goes with a new secret.

umask 077
openssl rand -hex 32 | tr -d '\n' > stream-secret.txt      # there must be no newline at the end
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. Address format: wss:// only, no user name, # or spaces, ASCII only (other characters percent-encoded), at most 2,048 characters. The host is a name with a dot or a public IP (localhost is not allowed), and the port is 443 or 1024โ€“65535. Otherwise 400 STREAM_URL_INVALID: โ€ฆ.
  2. Probe limits are counted the moment you ask: 5 an hour per bot, 10 per owner, and at the same time 1 per bot and 2 per owner. Beyond that, 429. Probes that did not connect anywhere because the gateway was busy do not count.
  3. Address check: if the name points to even one private or reserved address, or to Pabal's own address, you get 400 STREAM_URL_INVALID: the host must resolve only to public addresses.
  4. The probe: the gateway tries connecting once ("probe": true in start). If ready comes back, stream mode turns on, and the secret is kept encrypted under the server's master key. On failure you get 400 STREAM_PROBE_FAILED: <code> โ€” unreachable (name, connection, time or TLS), handshake_failed (an answer other than 101 โ€” usually the signature was rejected), redirect, no_subprotocol, no_ready, bad_ready, address_refused, busy, url and so on โ€” any other code comes as the gateway gave it. The server waits up to 12 seconds.
  5. When stream mode is turned on or its address is changed through the Bot API (the token), @BotFather sends the bot owner "The stream address was changed to โ€ฆ (secret โ€ฆ)" (์›์Œ ์ฃผ์†Œ๊ฐ€ โ€ฆ ๋กœ ๋ฐ”๋€Œ์—ˆ์–ด์š” (๋น„๋ฐ€ โ€ฆ)) with a Back to managed (๊ด€๋ฆฌํ˜•์œผ๋กœ ๋Œ์•„๊ฐ€๊ธฐ) button; when the bot goes back to managed, it says so, with a Voice calls (์Œ์„ฑ ํ†ตํ™”) button. This is to stop audio from quietly going to another server if the token leaks.
  • Putting stream_secret in the query string gives 400 before anything else. Stream mode cannot be set in a webhook response.
  • To go back to managed, use mode=managed (this deletes the address and the secret). If voice is on and the instructions are empty, you get VOICE_INSTRUCTIONS_REQUIRED, so send instructions along with it.
  • On a server where the operator takes only allowed bots, the bot takes calls only after the operator reviews the stream address and allows it again.
  • getVoiceCallSettings shows only stream_url and the secret's fingerprint. sendVoiceCallContext does not work for stream calls (VOICE_CONTEXT_NOT_SUPPORTED), and endVoiceCall hangs up at once, without a farewell.

Wire format

  • TLS 1.2 or later, with a public certificate that matches the name in the URL. The gateway connects to the IP it checked, but SNI and the certificate check use the name. Redirects (3xx) are not followed.
  • The subprotocol pabal.voice.v1 must be chosen (Sec-WebSocket-Protocol). No compression. Messages up to 65,536 bytes, text messages up to 4,096 bytes (UTF-8).
  • The gateway sends a ping every 15 seconds. If no pong comes within 15 seconds, it disconnects (most libraries answer on their own).
  • Text messages are JSON events; binary messages are audio. There is one audio format: PCM16 little-endian, mono, 24,000 Hz. call_id and bot_id are decimal strings.
DirectionMessageWhen ยท meaning
Pabal โ†’ bot{"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}Once, right after the upgrade. There is no caller name, number or ID (only caller.lang โ€” information about the person comes in the voice_call update)
Pabal โ†’ bot{"event":"live"}The AI notice has finished. From now on audio flows
Pabal โ†’ botBinaryThe caller's audio, always 960 bytes (20 ms). Only after live
Pabal โ†’ bot{"event":"mark","name":"โ€ฆ"}The audio before the bot's mark of that name has all been played
Pabal โ†’ bot{"event":"cleared","played_ms":N,"dropped_ms":N,"marks":["โ€ฆ"]}The answer to clear
Pabal โ†’ bot{"event":"buffer_full","dropped_ms":N}The bot's audio piled up beyond 30 seconds and the overflow was dropped
Pabal โ†’ bot{"event":"stop","reason":"โ€ฆ","played_ms":N,"duration_ms":N}The call has ended. Closed with 1000 shortly after
Bot โ†’ Pabal{"event":"ready"}Within 3 seconds after start, before anything else
Bot โ†’ PabalBinaryThe bot's audio: an even number of bytes, 2โ€“65,536 bytes. Anything before live is dropped
Bot โ†’ Pabal{"event":"clear"}Barge-in: stop the queued bot audio now
Bot โ†’ Pabal{"event":"mark","name":"[A-Za-z0-9_.:-]{1,64}"}"Tell me when playback gets here". Up to 256 waiting per call
Bot โ†’ Pabal{"event":"hangup"}End the call. Queued audio plays first, for up to 5 seconds, before hanging up
  • Unknown events are ignored. Text that is not JSON, text over 4 KiB, and audio with an odd number of bytes or over 64 KiB are protocol violations and end the call (the caller hears an apology notice).
  • Sending limits: text events on average 50 per second (100 in a burst), audio on average 4 times real time (60 seconds' worth in a burst). If the bot socket does not read and the caller's audio piles up beyond 30 seconds (1,500 chunks), the call ends too.

Signature

The gateway adds this header to the upgrade request.

X-Pabal-Voice-Signature: t=<Unix seconds>,nonce=<16 bytes, 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 is the host of the URL you gave Pabal (lower case, no port). Do not compute it from the request's Host header โ€” the sender writes that value, so it would not bind the signature to the host. path?query is the URL's path and query as they are (/ if there are none). In a probe, call is the probe's number.
  • The bot server must check: |now โˆ’ t| โ‰ค 300 seconds, each nonce used only once, and the HMAC compared byte by byte with a constant-time function. If anything is wrong, answer with something other than 101, such as 401.
  • Then it checks that start.call_id equals the signature's call. If not, it closes with 1008 โ€” so a signed upgrade cannot be reused for another call. A server that serves only one bot also checks that start.bot_id is its bot.
  • Test values: secret example-stream-secret-of-the-bot-developer-0001, t=1790000000, nonce 00112233445566778899aabbccddeeff, call 4611686018427387905, host bot.example.com, path /voice?bot=100042 โ†’ v1=807517ff96ac75223bde35b48c775f49a2eda2fb4832a02c0ea832b914d70d0c.

Audio ยท barge-in ยท mark ยท ending

  • Bot โ†’ caller buffer: the gateway plays audio out at playback speed, so you can send it all at once, but anything piled up beyond 30 seconds is dropped and buffer_full is sent. When the bot's audio pauses for 300 ms, one "chunk" is considered finished (the unit for mark and clear).
  • clear: drops all bot audio not yet played, and playback goes silent from the next 10 ms frame (measured: under 50 ms). In the answer cleared, played_ms is the total bot audio actually played in this call so far, dropped_ms is how much was just dropped, and marks are the names of the marks on the dropped chunks โ€” use them to tell your model "where it was cut off".
  • mark: comes back with the same name once the audio before it has been played. It is best to end the audio before a mark on a multiple of 480 bytes (10 ms); any leftover piece is padded with silence.
  • Ending: the bot sends hangup or closes cleanly with 1000 or 1001. Any other close or a dropped TCP connection ends the call like a protocol violation (with an apology notice to the caller). When Pabal ends the call, stop.reason is a short code โ€” hangup (the caller hung up), max_length, bot_ended, disconnect, drain, kill_switch, operator, link_lost, media_failed โ€ฆ Treat unknown values as "ended".
  • A probe's start has "probe": true, an empty caller and max_seconds: 0. After you send ready, {"event":"stop","reason":"probe"} comes and the connection closes. Do not open anything that costs money, such as a model session, for a probe.

Example โ€” the smallest stream server

A server that checks the whole wire format without any AI: on live it plays a chime, and after that it sends the caller's audio straight back (echo). It checks the signature, time, nonce, call and bot. First use it to see a call work end to end, then put your AI in place of the echo. The Pabal repository also has scripts/bots/stream_echo_bot.py (with a barge-in demo) and scripts/bots/stream_openai_relay.mjs (relays to Realtime with your OpenAI key; an example that has not been run); ask the server's operator for them if you need them.

# stream_bot.py โ€” pip install "websockets>=13"
# Run: STREAM_SECRET_FILE=stream-secret.txt TLS_CERT=fullchain.pem TLS_KEY=privkey.pem \
#      PUBLIC_HOST=bot.example.com BOT_ID=<the bot ID โ€” the number before the : in the token> 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()   # the host of the stream_url you gave Pabal (not the Host header)
BOT_ID = os.environ.get("BOT_ID", "")             # empty: take calls for any bot
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 for 0.6 s, PCM16 24 kHz
seen = {}                                         # nonces already used


def signed_call(header, target):
    """Checks X-Pabal-Voice-Signature and returns the signed call id, or None if it is wrong."""
    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")):  # byte by byte
        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 must name this call
    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"}))    # within 3 seconds, before anything else
    live = False
    async for message in ws:
        if isinstance(message, bytes):
            if live:
                await ws.send(message)            # send the caller's 20 ms straight back (echo)
            continue
        event = json.loads(message)
        if event.get("event") == "live" and not start.get("probe"):   # the AI notice has finished
            live = True
            await ws.send(CHIME)
            await ws.send(json.dumps({"event": "mark", "name": "chime"}))
        elif event.get("event") == "stop":
            print("Ended:", 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"Listening on wss://{PUBLIC_HOST}:{PORT}{PATH}", flush=True)
        await asyncio.get_running_loop().create_future()


asyncio.run(main())

What callers see

  • Call button: a call button appears in the voice bot's chat header and profile. It appears only when the server decides it would take a call from this person on this device (before public launch, only for testers). There are no video calls.
  • Confirmation sheet (before the microphone permission): the title "An AI answers this call"; that the bot is not a person and the voice is AI-generated; that the call is not end-to-end encrypted; who receives the audio (Google, the Gemini Live API in Vertex AI) and the country and region (United States, Iowa); what is sent (the caller's voice during the call, the conversation text and the AI's replies, notes the bot adds during the call and the results of the bot's functions); retention (Pabal does not store audio and keeps only the call record for 180 days; Google keeps nothing after producing its answer, but content flagged as possible abuse by automated checks may be logged for up to 90 days); what the bot owner receives (name, account ID, language, call time and length, values used by tools โ€” not the phone number or the audio); and 19 or older. The buttons are "I'm 19 or older ยท Call" and Cancel. The age is self-confirmed, not verified. The call button is the main button (filled with the accent color) and Cancel is the secondary one. On a server with conversation keeping on, the checkbox "Keep this call's conversation as text in my chat" (off by default) follows the explanation.
  • How often: desktop and Android ask the first time you call that bot (and again if the recipient or the sheet version changes); iPhone and Apple Watch ask every time. The watch shows the full sheet every time, and the conversation-keeping switch starts at the choice made in the last call to that bot. The app sends the consent to the server with every call, and the server accepts a consent only from the same device, for the same bot, within 2 minutes, and uses it for that one call only.
  • Once connected: the AI notice "You're now talking to an AI voice bot." (for a stream bot, "This is an AI voice bot. The call's audio goes to the bot developer's server.") โ†’ (for a transcript bot) the transcript notice โ†’ the bot's greeting. The bot owner cannot turn the notices off, and what the caller says before the notices end does not go to the model. Keeping the conversation has no spoken notice โ€” the caller chose it on the screen themselves.
  • Notice language: the notices are not in the bot's language but in the language of the confirmation sheet the caller read. All the notices of one call are in one language โ€” the AI notice, the transcript notice and the apology when the connection is lost (for a stream bot, the stream notice and the apology). If any of them has no reviewed recording in that language, they are all in English. Today the only reviewed languages are Korean and English. The confirmation sheets and notices in the other eight languages (Japanese, Chinese, French, Spanish, Indonesian, Vietnamese, Khmer and Arabic) are drafts awaiting legal review and a native speaker's check, so for now callers who use the app in those languages also see the English confirmation sheet and hear the English notices. The Khmer recordings and the Japanese version of "Just a moment longer, please." are still waiting for someone to listen to them and pick a take. The first notice of a call and the transcript notice always start after at least 300 ms of silence, so that the phone line does not swallow the first word.
  • Call screen: under the name, "@{username} ยท AI ยท Bot" and "AI-generated voice ยท Call audio is processed by Google". There is no emoji key and no encryption text.
  • If the connection has a problem, the caller hears "Sorry, we've lost the connection. Ending the call now." and the call ends. Every refusal ends with a busy signal.

The explanation for app users is in Using the app โ€” Calling a voice bot.

Calls from bots

Off on pabal.me โ€” waiting for the legal review

The server has it, but no bot can call anyone until the privacy policy's bot-calls-outbound section and the terms of service's voice-bot-owners-outbound section have passed the legal review and are published. No app build with the permission sheet has shipped yet either (Current status). What follows is how it works on a server where it is on.

A voice bot can also call a person first: to confirm a booking, to wake someone at a time they chose, to say a delivery is coming. The bot owner's code asks with startVoiceCall, the server judges the request and rings the person's devices, and if the person answers, Pabal's notice plays first and then the AI voice model speaks as the bot's instructions say. How it ended comes as a voice_call update.

  • Only people who allowed it. The person must allow that bot to call them in the Pabal app (The callee's permission), and can turn it off at any time.
  • Not for advertising. There are four purposes โ€” reminders, wake-up calls, deliveries and appointments โ€” and advertising, sales pitches and promotion are not allowed (bot owner terms, items 12โ€“24).
  • Calls from bots are not end-to-end encrypted โ€” the audio goes through Pabal's server and the AI provider. Once the person answers, the audio goes to Google during the call โ€” Vertex AI's Gemini Live API, in Iowa, USA (Google Cloud us-central1). Pabal does not store the audio.
  • No conversation text goes to the bot owner. Calls a bot makes have no transcript. What the bot owner gets is the five things listed under What the callee sees and hears.

How it flows

The bot's code Pabal server Gateway ยท model The person's app โ‘  startVoiceCall Judged: bot ยท permission ยท limits quiet hours ยท paused โ‘ก 200 {voice_call_id} โ‘ข dial โ‘ฃ Ring: name (AI) โ‘ค Answered โ‘ฅ Pabal's notice โ‘ฆ Bot: name ยท AI ยท purpose The call โ€” audio goes through the gateway and the AI model โ‘ง voice_call: started ยท ended No answer ยท no ring: not_answered
One call from a bot: request โ†’ judgement โ†’ ring โ†’ answer โ†’ notice โ†’ first words โ†’ end

About the diagram

  • Four lanes: from the left, the bot owner's code, Pabal's server, the voice gateway with the AI model, and the callee's app. The dotted vertical lines are time; the blue solid lines are the call going ahead, and the gray line is the answer to the bot.
  • The gray box at โ‘  โ€” three judgements: before answering, the bot's side (calling turned on, the operator's review, the terms, the limits) and whether there is a permission; after the 200, the person's side (quiet hours, paused, the person's daily limit and gap, a device to ring); and once more right before the ring.
  • The 200 at โ‘ก only means "taken". Whether it rang and whether it was answered come only in the updates at โ‘ง.
  • โ‘ฃโ€“โ‘ฆ, the person's side: the ring shows "(AI)" after the bot's name, and on answering, Pabal's notice (โ‘ฅ) plays before the bot. In its first words (โ‘ฆ) the bot says its name, that it is an AI, and the purpose of the call. During the green box the audio goes through the gateway and the AI model โ€” it is not end-to-end encrypted.
  • The red dashed line is the point: a request that never rang because the person's side refused it and one that rang with nobody answering end the same way, at the same time, as not_answered. The bot cannot tell whether the person was asleep or on another call.

Turning it on โ€” @BotFather

Calls from a bot are turned on by the bot owner in @BotFather only, never with the token (sending calling fields to setVoiceCallSettings gives 400 VOICE_FIELD_NOT_AVAILABLE). Voice calls must be on first. /mybots โ†’ the bot โ†’ Voice calls (์Œ์„ฑ ํ†ตํ™”) โ†’ Calls from the bot (๋ด‡์ด ๊ฑฐ๋Š” ์ „ํ™”) โ†’ Turn on (์ผœ๊ธฐ).

์ผœ๊ธฐ
BotFather@order_helper_bot ์ด ์‚ฌ๋žŒ์—๊ฒŒ ์ „ํ™”๋ฅผ ๊ฑธ ์ˆ˜ ์žˆ๊ฒŒ ํ• ๊นŒ์š”? โ€ข ์ด ๋ด‡์—๊ฒŒ ์ „ํ™”๋ฅผ ํ—ˆ๋ฝํ•œ ์‚ฌ๋žŒ์—๊ฒŒ๋งŒ ๊ฑธ์–ด์š”. ํ—ˆ๋ฝ์€ ๊ทธ ์‚ฌ๋žŒ์ด ํŒŒ๋ฐœ ์•ฑ์—์„œ ํ•˜๊ณ , ์–ธ์ œ๋“  ๋Œ ์ˆ˜ ์žˆ์–ด์š”. โ€ข ๊ทธ ์‚ฌ๋žŒ์˜ ์กฐ์šฉํ•œ ์‹œ๊ฐ„๊ณผ ์‰ฌ๋Š” ์ค‘์—๋Š” ์šธ๋ฆฌ์ง€ ์•Š์•„์š”. โ€ข ํ•˜๋ฃจ ์ƒํ•œ(์ง€๊ธˆ 50๋ฒˆ)๊ณผ ์„œ๋ฒ„์˜ ํ•œ๋„๊ฐ€ ์žˆ์–ด์š”. โ€ข ์šด์˜์ž๊ฐ€ ์ด ๋ด‡์˜ ์ด๋ฆ„ยท์‚ฌ์ง„ยท์ง€์‹œ๋ฌธยท๋ชฉ์†Œ๋ฆฌยท๋ชฉ์ ์„ ๊ฒ€ํ† ํ•˜๊ณ  ํ—ˆ๋ฝํ•ด์•ผ ๊ฑธ๋ ค์š”. ๋ฌด์—‡์ด๋“  ๋ฐ”๋€Œ๋ฉด ๋‹ค์‹œ ๊ฒ€ํ† ํ•ด์š”. โ€ข ๋ด‡์ด ๊ฑฐ๋Š” ์ „ํ™”๋Š” ์ข…๋‹จ ๊ฐ„ ์•”ํ˜ธํ™”๊ฐ€ ์•„๋‹ˆ์—์š”. ํ†ตํ™” ์†Œ๋ฆฌ๋Š” ํŒŒ๋ฐœ ์„œ๋ฒ„์™€ AI ์ œ๊ณต์ž๋ฅผ ๊ฑฐ์ณ์š”.
์ผœ๊ธฐ
BotFather๋ด‡์ด ๊ฑฐ๋Š” ์ „ํ™”๋ฅผ ์ผฐ์–ด์š”. ์šด์˜์ž๊ฐ€ ์ด ๋ด‡์„ ๊ฒ€ํ† ํ•˜๊ณ  ํ—ˆ๋ฝํ•˜๋ฉด Bot API ์˜ startVoiceCall ๋กœ ๊ฑธ ์ˆ˜ ์žˆ์–ด์š”. ๋ด‡์ด ๊ฑฐ๋Š” ์ „ํ™” ยท @order_helper_bot ์ƒํƒœ: ์ผœ์ง ์šด์˜์ž ํ—ˆ๋ฝ: ๋Œ€๊ธฐ ๋ชฉ์ : ๋ฐฐ๋‹ฌ ํ•˜๋ฃจ ์ƒํ•œ: 50๋ฒˆ ์˜ค๋Š˜: ์š”์ฒญ 0 ยท ๋ฐ›์Œ 0 ยท ์•ˆ ๋ฐ›์Œ 0 ์ด ๋ด‡์—๊ฒŒ ์ „ํ™”๋ฅผ ํ—ˆ๋ฝํ•œ ์‚ฌ๋žŒ์—๊ฒŒ๋งŒ, ๊ทธ ์‚ฌ๋žŒ์˜ ์กฐ์šฉํ•œ ์‹œ๊ฐ„๊ณผ ์‰ฌ๋Š” ์ค‘์„ ํ”ผํ•ด ๊ฑธ์–ด์š”. ๋ด‡์ด ๊ฑฐ๋Š” ์ „ํ™”๋Š” ์ข…๋‹จ ๊ฐ„ ์•”ํ˜ธํ™”๊ฐ€ ์•„๋‹ˆ์—์š” โ€” ํ†ตํ™” ์†Œ๋ฆฌ๋Š” ํŒŒ๋ฐœ ์„œ๋ฒ„์™€ AI ์ œ๊ณต์ž๋ฅผ ๊ฑฐ์ณ์š”.

BotFather speaks Korean. The first message asks whether the bot may call people: only people who allowed it, never in their quiet hours or while they are paused, within a daily cap (50 now) and the server's limits, only after the operator has reviewed and allowed the bot's name, photo, instructions, voice and purposes (and again whenever any of them changes); calls from bots are not end-to-end encrypted. The second shows the bot's screen once it is on: on, waiting for the operator, purpose delivery, cap 50, today's requests.

  • Terms, version 4: if you have not agreed yet, the bot owner terms come before turning it on. Items 12โ€“24 are the promises for calls from bots.
  • Purposes: it turns on only with at least one of reminder (reminders) ยท wake_up (wake-up calls) ยท delivery (deliveries) ยท appointment (appointments). Every request names one of them; a purpose the bot did not choose is a 400. Changing the purposes asks the people who allowed it again, and the operator reviews again.
  • Daily cap: how many requests the bot may send a day โ€” 10, 20 or 50 (50 by default).
  • The operator's review: the operator looks at the bot's name, @username, photo, instructions, greeting, tools, voice, language, mode, purposes and the owner's name on the admin page and allows it. If any of them changes, the bot cannot call until it is allowed again (403 VOICE_OUTBOUND_OFF: review).
  • The token: changing the voice settings with the token (except turning voice off) freezes calls from the bot for 24 hours (frozen). If it was you, lift it with I changed it (๋‚ด๊ฐ€ ๋ฐ”๊ฟจ์–ด์š”, vdkยทvdK) in @BotFather. Reissuing the token also freezes it for 24 hours, ends its calls in progress and deletes the webhook of a bot whose calls are on.
  • Only bots whose language is Korean or English (ko-KRยทen-US) can call. A bot in stream mode, with its own key, tools or MCP servers cannot (mode).

The callee's permission

A bot cannot get the permission itself. The person opens that bot's permission sheet in the Pabal app and taps "I am 19 or older ยท Allow". The sheet says an AI makes these calls, the purposes, the bot owner's name, where the audio goes, and the quiet hours (Using the app โ€” Letting a bot call you). There are three ways to open it.

  • The bot's menu: "Allow callsโ€ฆ" in the bot's chat or profile.
  • A link in the one-to-one chat: the bot sends https://pabal.me/<bot username>?callme=1 to the person's one-to-one chat with it, as a link in the text or an inline URL button. The sheet opens only for a message that bot sent in that chat โ€” a link forwarded to another chat, a preview or the web only open the chat. A person can open the sheet from a link once a day (all bots together), and not at all if they never wrote to the bot.
  • The settings list "Bots allowed to call me": see the bots allowed, turn them off, allow them again.
curl -s "https://pabal.me/bot$BOT_TOKEN/sendMessage" -d chat_id=$USER_ID \
  --data-urlencode "text=Shall I call you before your order arrives?" \
  --data-urlencode 'reply_markup={"inline_keyboard": [[{"text": "Allow calls", "url": "https://pabal.me/order_helper_bot?callme=1"}]]}'
  • Self-hosted servers: the apps know only pabal.me links. For another server, its host must be added to the link rules of the app build before a link opens the sheet (the menu and the settings list work as they are).
  • For now: only accounts with a +82 number, with the device's time zone set to Korea (Seoul), on a device signed in for at least 24 hours can turn it on (the operator sets the countries). Someone who answers that they are under 19 cannot have calls with bots for a year.
  • When the bot's name, owner or purposes change, or a year after the permission, the person is asked again; until then the bot cannot call.

Calling โ€” startVoiceCall

curl -s "https://pabal.me/bot$BOT_TOKEN/startVoiceCall" -d user_id=$USER_ID -d purpose=delivery \
  --data-urlencode "context=Order A-1042 arrives today around 3 p.m. Ask whether they can take it then." \
  -d max_duration=180 -d ring_timeout=30
{"ok": true, "result": {"voice_call_id": "8812345678901234"}}
ParameterMeaning
user_idThe id of the person to call, who must have allowed this bot
purposeOne of the purposes this bot chose: reminder ยท wake_up ยท delivery ยท appointment
contextWhat the model is told about this call, for the model only: 1โ€“1,000 characters, 3,072 bytes. Checked against the content rules (impersonation, hiding the AI, asking for secrets, advertising). The model uses it after its first words (name, AI, purpose), once the person has answered
max_durationThe longest the call may last once answered (seconds): 30 up to 300 or the bot's own maximum, whichever is smaller. 180 by default
ring_timeoutHow long it rings (seconds), 30โ€“45. 40 by default
AnswerMeaningWhat to do
200 {"voice_call_id": "โ€ฆ"}Taken. Whether it rang and whether it was answered come in the updatesโ€”
400 Bad Request: <what>A wrong parameter, a purpose the bot did not choose (purpose is not declared), a bot language other than Korean or EnglishFix it
400 VOICE_CONTEXT_REFUSED: <code>context breaks the content rulesFix it
403 VOICE_OUTBOUND_OFF: <reason>The bot's side: owner_off (not turned on) ยท terms (terms version 4) ยท review (waiting for the operator's review) ยท suspended (automatic stop) ยท frozen (token freeze) ยท owner_name (the owner's name fails the impersonation check) ยท mode, or a bot-side reason of the voice settings (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. The server-side reasons voice_off ยท kill_switch ยท no_relay ยท outbound_off come as 503, not 403@BotFather ยท the operator
403 USER_UNAVAILABLEThis person cannot be called: no permission, turned off or asked again, blocked, not a person, out of scope, under 19, not a tester. The body is always the same, whatever the reasonAsk for the permission
429 retry after NA limit on the bot's or owner's side: the request rate, the bot's or owner's daily requests, the bot's concurrent calls, the owner's outbound minutes, the bot's or owner's daily call minutes (shared with calls to the bot)After N seconds
503 VOICE_UNAVAILABLEThe server's side: the outbound switch is off, voice is off on the server (also when the privacy policy has no bot-call section), the kill switch, no call relay, the server's limits and outbound share, no gateway that can place calls (or all of them are full), the request could not be recordedLater
{"ok": false, "error_code": 403, "description": "Forbidden: USER_UNAVAILABLE"}

The person's side is judged after the 200. Even with a permission, in the person's quiet hours, while the pair is paused, past the daily limit with that person or the person's own daily limit and gap, with no device to ring, or while the person is on another call, nothing rings and not_answered comes about ring_timeout + 12 seconds after the request โ€” the same as a ring nobody answered. A permission does not change the bot's side (403, 429, 503).

Settings and usage

"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}
}
  • outbound in getVoiceCallSettings is always there: what was set in @BotFather (enabledยทpurposesยทdaily_cap), the operator's review (reviewed), when a token freeze ends (frozen_until, 0 when not frozen), the automatic stop (suspended) and whether the bot can call now (available). unavailable_reason is there only when available is false (on the bot's side, the same values as the reasons of 403 VOICE_OUTBOUND_OFF; on the server's side, voice_off ยท kill_switch ยท no_relay ยท outbound_off โ€” these four go with 503 VOICE_UNAVAILABLE).
  • outbound in getVoiceCallUsage is there only for a bot that has ever turned calling on: today's requests, answered, not answered and minutes, and the daily limits that apply to this bot. How many rang is not given. callsยทsecondsยทmissedยทrefused never count calls the bot made. But calls the bot makes spend the same daily call minutes of the bot and its owner as calls to the bot: an answered call's seconds are charged, so remaining_seconds_today's bot and owner go down โ€” and while a call rings they are already down by what it holds, which comes back if nobody answers.
  • setVoiceCallSettings cannot change them (400 VOICE_FIELD_NOT_AVAILABLE).

Updates

They come as the same voice_call updates as calls to the bot (name voice_call in allowed_updates). direction is "outgoing", and the person appears only as the id in to (no name, username or phone number).

{
  "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
  }
}
eventWhenExtra fields
startedThe person answered and the call connected. On an Apple Watch, when the watch's audio joined (if it never does, a 0-second ended with failed)max_duration
endedAn answered call ended. It arrives up to 10 seconds late whatever the server's settings, while the server waits for the gateway's usagestart_date, duration, max_duration, end_reason
not_answeredA request nobody answered โ€” whether it rang unanswered, was declined, or never rang because of the person's side. About ring_timeout + 12 seconds after the requestNone
failedThe link to the gateway that took the request was lost before an answerNone

end_reason: callee (the person hung up) ยท bot (the bot hung up, or its token was reissued) ยท max_length (the longest length) ยท failed (the connection failed after the answer) ยท operator (the operator or the kill switch) ยท suspended (the bot or owner suspended). Calls a bot makes have no missed, refused or ringing โ€” how fast a person answers would show whether they were by their phone โ€” and no transcript (off by design).

During the call

  • endVoiceCall: before the bot is told how it ended, it withdraws the request whether it is ringing or not yet (true, then not_answered). It ends an answered call (end_reason bot). Withdrawing within 15 seconds of the request pauses the pair for an hour.
  • sendVoiceCallContext: only after the person answered (before that, VOICE_CALL_NOT_FOUND). Three per call, 1,000 characters and 3,072 bytes, the same content rules as context. If the gateway does not take notes, VOICE_CONTEXT_NOT_SUPPORTED.

What the callee sees and hears

  • The ring: "(AI)" after the bot's name, and "AI voice bot ยท Pabal never asks for codes".
  • On answering, a notice plays before the bot: "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." โ€” in the language of the permission sheet the person read (Korean or English); from an app in another language, in English. The bot owner cannot turn it off.
  • The bot's first words: the bot's name, that it is an AI, and the purpose of the call. context comes after that.
  • Ways to turn it off: "Stop calls from this bot" in the menu, blocking the bot and reporting it always turn it off. Saying clearly during a call something like "never call me again" turns it off on the spot (a plain "stop" is up to the model).
  • Paused: after a decline, a ring left unanswered, or hanging up within 10 seconds of answering, the bot cannot call that person for 24 hours.
  • The missed-call record: when a device really rang, the person has not blocked the bot and the bot's chat has no unread message, it stays as "Missed AI bot call" (read, no notification). With an unread message, or when it never rang because of quiet hours or a pause, there is no record. Every request a bot made is kept in the report evidence (30 days) without exception.
  • What the bot owner gets (in the permission sheet's order):
    1. the person's Pabal id
    2. whether the permission is on (the bot's call requests show it)
    3. whether the person answered
    4. when the call started and how long it lasted
    5. who hung up
    A missed call and a declined one look the same. What was said in the call is not sent.
  • Shared devices: any signed-in device can answer, and the bot says the purpose and what it was told to whoever answers. On a shared computer, tell people to sign out.
  • The lock screen: on Android 14 and later, unless the person allows full-screen notifications, the call comes as a notification instead of a call screen over the lock screen. iPhones and Apple Watches ring like a phone call.
  • Keeping the conversation keeps it only in the person's own chat, if they choose it on the permission sheet. With Answer on Watch on in the Pabal Watch app, the Apple Watch rings too.
  • Calls from bots are not end-to-end encrypted โ€” the audio goes through Pabal's server and the AI provider.

Limits ยท quiet hours ยท paused ยท automatic stop

LimitDefaultOn a production server
One bot to one person (a day)3 callsThe operator can only lower it
To one person (a day, all bots)6 callsCan only be lowered
Gap between calls to one person10 minutesCan only be raised
One bot's requests (a day)50 callsThe owner picks 10, 20 or 50 in @BotFather
One bot owner (a day, all bots)100 calls ยท 30 minutesCalls can only be lowered
Until the bot may send its next request10 secondsCan only be raised
The server's outbound minutes30% of the server's daily minutesCan only be lowered
Quiet hours21:00โ€“08:00The operator and the person can only widen them. In the device's time zone
Wake-up window (wake_up only)at most 120 minutesThe person picks it inside the quiet hours
Ring (ring_timeout)30โ€“45 seconds40 seconds when the request does not say
Paused โ€” a decline, a ring left unanswered, a hang-up within 10 seconds of answering24 hoursCan only be raised
Paused โ€” the bot withdrew within 15 seconds1 hourCan only be raised
  • "A day" is a day in the server's time zone (TELEGRAM_VOICE_TIME_ZONE). A call never lasts more than 300 seconds.
  • Calls from bots also spend the same daily call minutes as calls to the bot (the bot's 60 minutes ยท the owner's 120 minutes). Once they are used up, startVoiceCall gets 429, and calls to the bot are refused too (refused, budget).
  • A pause ends when the time is up, when the person calls the bot, taps "I can take calls now", or allows it again. A message or button from the bot does not end it.
  • The automatic stop stops only the bot's calls to people (403 VOICE_OUTBOUND_OFF: suspended) and lasts until the operator checks and lifts it: an answer rate under 20% over the last 20 calls, 3 reports within 7 days from people it rang, 30% or more of its people turning the permission off (with at least 10), 2 calls caught by the rules during the call, 5 withdrawals within 15 seconds, or 2 or fewer people answering with 60 or more minutes answered.

Limits

LimitDefaultWho sets it
Longest callBot 300 seconds (range 60 up to the operator's and server's cap, default 600)The bot owner, within the operator's and server's cap. The @BotFather buttons are 1, 3, 5 and 10 minutes
The bot's concurrent calls2 (1 up to the operator's and server's cap, default 2)The bot owner, within the operator's and server's cap
Daily call minutes per bot60 minutesOperator
Daily minutes per bot owner (all bots together)120 minutesOperator
Daily minutes for the whole server300 minutesOperator
Per caller60 minutes a day, 20 calls an hourOperator
Shortest call taken60 seconds โ€” if less time than this is left, the call is not takenOperator
New accountsPeople who signed up less than 7 days ago cannot call โ€” except testers, and people the bot sent a message to more than a day agoOperator
Changing settings30 an hour per botโ€”
Notes during a call10 per minute per callโ€”
Tools16 tools, 8 KB; open queries: 2 per call, 8 per bot; tool calls: 30 a minute per call, 120 per botโ€”
Own-key tests ยท stream probes5 an hour per bot, 10 per ownerโ€”
MCP registrations and checks10 an hour per bot, 20 per owner, 2 servers per botโ€”

From the Apple Watch you can't call bots that use tools, MCP servers, an own key, stream mode or the bot owner's transcripts. This refusal goes only to the caller (the watch app's 403 WATCH_NOT_YET); no refused update goes to the bot.

The length of a call is the smallest of the caller's, bot's, owner's and server's time left today and the call cap. When the call is taken, that much is reserved on all four limits, and when it ends, only the actual seconds by the server's clock are charged. Own-key calls do not use the bot, owner or server limits (the caller's limits still apply).

Errors

Errors look the same as for other methods: {"ok": false, "error_code": 400, "description": "Bad Request: VOICE_โ€ฆ"}. 403 reads Forbidden: <code>: <description> (except USER_UNAVAILABLE, which is always exactly Forbidden: USER_UNAVAILABLE, with no description), and 429 comes with parameters.retry_after.

CodeMeaning
403 VOICE_RULES_NOT_ACCEPTEDThe voice-bot rules must first be accepted in @BotFather
403 VOICE_NOT_AVAILABLEThis bot cannot use voice on this server (voice off, or not allowed by the operator) โ€” settings cannot be changed; only turning off and deleting work
403 VOICE_SUSPENDEDThe operator has suspended voice for this bot. Only turning off works
403 VOICE_OWNER_TERMS_NOT_ACCEPTEDThe bot owner terms (version 4) must be accepted first
403 VOICE_OWN_KEY_NOT_AVAILABLEThis server does not accept own keys
403 VOICE_STREAM_NOT_AVAILABLEStream mode cannot be used on this server right now
403 VOICE_OUTBOUND_OFF: <reason>This bot cannot call people now: owner_off ยท terms ยท review ยท suspended ยท frozen ยท owner_name ยท mode, or a bot-side reason of the voice settings (Calling). voice_off ยท kill_switch ยท no_relay ยท outbound_off come as 503
403 USER_UNAVAILABLEThis person cannot be called โ€” always the same body whatever the reason, no permission included
503 VOICE_UNAVAILABLEThe server cannot place calls to people right now (the outbound switch, voice off on the server, the kill switch, no call relay, the server's limits, gateways, a request that could not be recorded) โ€” try again later
Bad Request: purpose is not declared, Bad Request: the bot's voice language must be ko or en for outbound callsstartVoiceCall: a purpose the bot did not choose, a bot language other than Korean or English
VOICE_INSTRUCTIONS_REQUIRED, VOICE_INSTRUCTIONS_TOO_LONG, VOICE_GREETING_TOO_LONGInstructions are required ยท instructions over 8,192 bytes ยท greeting over 512 bytes
VOICE_INVALID, VOICE_LANGUAGE_INVALID, VOICE_LIMIT_INVALID, VOICE_MODE_INVALIDNo such voice ยท language ยท a limit out of range ยท a mode other than managed/stream
VOICE_CONTENT_REFUSED: <field>: <code>, VOICE_CONTEXT_REFUSED: <code>Content rules (settings, the context of startVoiceCall)
VOICE_TOOL_INVALID: <path>: <reason>Tool definitions (defining tools)
VOICE_FIELD_NOT_AVAILABLE: โ€ฆFields that are not accepted (model, api_key), stream fields on a managed bot, or the fields of calls from bots (outboundยทpurposesยทdaily_cap โ€” @BotFather only)
VOICE_CALL_NOT_FOUND, VOICE_CALL_NOT_ACTIVE, VOICE_CONTEXT_NOT_SUPPORTED, VOICE_CONTEXT_TOO_LONGMethods used during a call (during a call)
VOICE_USAGE_PERIOD_INVALIDThe period for getVoiceCallUsage
TOOL_QUERY_ID_INVALID, TOOL_QUERY_EXPIRED: โ€ฆ, TOOL_RESULT_โ€ฆ, TOOL_ERROR_TOO_LONGTool answers (answering)
API_KEY_INVALID, API_KEY_NO_REALTIME_ACCESS, 503 API_KEY_CHECK_UNAVAILABLE, VOICE_PROVIDER_INVALIDOwn keys (own model key)
STREAM_URL_INVALID, STREAM_SECRET_REQUIRED, STREAM_SECRET_INVALID, STREAM_PROBE_FAILED: โ€ฆStream mode (setup and the probe)
MCP_โ€ฆ, MCP_OPT_IN_REQUIREDMCP servers (registering). For 503 VOICE_NOT_AVAILABLE: mcp: โ€ฆ try again a little later; for 409 MCP_CHECK_IN_PROGRESS try again after the check finishes
429 Too Many Requests: retry after NAn hourly limit (settings, key tests, probes, MCP), the notes limit, or a limit of calls from bots (limits)

Bots connected over MTProto

  • A bot signed in only over MTProto calls the voice methods with bots.sendCustomRequest (the method name, and the same JSON). Supported: setVoiceCallSettings, getVoiceCallSettings, answerVoiceCallToolQuery, endVoiceCall, sendVoiceCallContext, getVoiceCallUsage, startVoiceCall and the five MCP methods. Anything else gives METHOD_INVALID. The answer to startVoiceCall is {"voice_call_id": "โ€ฆ"}, and refusals come as RPC errors (USER_UNAVAILABLE, VOICE_OUTBOUND_OFF: review, FLOOD_WAIT_n, VOICE_UNAVAILABLE).
  • Methods a webhook answer cannot call: setVoiceCallApiKey, startVoiceCall and the MCP registration and key methods. Put in the body of the answer to a webhook POST, they are not run โ€” so that no message a person sent turns straight into a phone call through a webhook answer. Call them with a request of their own.
  • Own keys only over HTTPS: setVoiceCallApiKey and deleteVoiceCallApiKey cannot be called with sendCustomRequest. To delete a key, use the HTTP Bot API or @BotFather.
  • For a bot with no webhook that has not called getUpdates within the last 15 seconds, voice_call (that of a call the bot made too) is pushed once as updateBotWebhookJSON (regardless of allowed_updates, and not sent again). Tool queries come as updateBotWebhookJSONQuery; answer with {"result": โ€ฆ} or {"error": "โ€ฆ"} through bots.answerWebhookJSONQuery. voice_call_transcript does not arrive over MTProto (the ended update's transcript does).
  • For RPC errors, 429 becomes FLOOD_WAIT_n, and the rest are the uppercase codes as they are.