Docs para desarrolladores
Español

Desarrollo de bots

Bots de voz

Cuando alguien llama a un bot desde la aplicación, contesta un modelo de voz de IA. Cómo activarlo, instrucciones y voces, avisos de llamada, herramientas, servidores MCP, clave propia, transcripciones, modo stream, Apple Watch y «Guardar conversación»: una guía para propietarios de bots.

Un bot de voz es un bot al que se puede llamar desde la aplicación. Quien contesta no es el código del propietario del bot, sino el modelo de voz de IA de Pabal; el propietario del bot define con la Bot API las instrucciones, el saludo, la voz, el idioma y los límites que debe seguir ese modelo. Cada vez que empieza o termina una llamada recibes una actualización voice_call, y durante la llamada puedes dejar que el modelo llame al código del bot (herramientas) para buscar un pedido o comprobar una reserva. También existe un modo stream en el que tu propio servidor recibe el audio directamente en lugar del modelo de IA. Si es tu primer bot, empieza por Crear un bot.

Estado actual

Los bots de voz tienen que estar activados en cada servidor, y en la última revisión de esta página (2026-10-02) están desactivados en pabal.me: se están probando en el servidor de desarrollo

Solo se puede llamar a un bot de voz en un servidor que cumple todas las condiciones de abajo. Los métodos existen también en un servidor con la voz desactivada, así que se pueden leer los ajustes. Pero mientras la voz no esté activada en ese servidor y el operador no haya autorizado el bot, no se guarda ningún ajuste salvo los que desactivan algo (desactivar la voz, desactivar la transcripción, salir del modo stream, borrar tu clave propia, eliminar un servidor MCP): 403 VOICE_NOT_AVAILABLE. Para saber si tu bot puede recibir llamadas ahora mismo, mira available y unavailable_reason en getVoiceCallSettings.

FunciónPara activarla (todas las condiciones)pabal.me hoy
Llamadas a bots de vozEl servidor tiene conectados una pasarela de voz y un relé de llamadas (TURN) · la política de privacidad publica la sección de llamadas a bots (bot-calls) · el operador autorizó el bot · antes de la apertura al público solo pueden llamar los testers que elige el operador · la aplicación de quien llama es una compilación con la pantalla de confirmación (escritorio pabal-bc2 —mientras la pantalla de confirmación solo exista en coreano e inglés, también se acepta pabal-bc1—, Android 62151, iPhone compilación 5 o superior, app del reloj para Apple Watch compilación 1 o superior) — esas compilaciones aún no se han publicadoDesactivado
Llamar desde el Apple WatchLlamadas a bots de voz + la API del reloj del servidor + el punto de entrada de audio del reloj (pabal-voice-edge) + una pasarela que acepte llamadas directas (direct) · compilación de la app del reloj igual o superior a TELEGRAM_VOICE_MIN_BUILD_WATCHOS · todavía no para bots que usan herramientas, servidores MCP, clave propia, modo stream o la transcripción del propietario del botDesactivado: en pruebas en el servidor de desarrollo
Guardar conversación (copia de quien llama)Llamadas a bots de voz + la sección bot-calls-keep-text de la política + una pasarela que declare keep_text (0.9.0) + una compilación de la aplicación con la casilla · revisión legal del texto de la pantalla de confirmaciónDesactivado: pendiente de revisión legal
Llamadas de botsEstán publicadas la sección bot-calls-outbound de la política de privacidad y la sección voice-bot-owners-outbound de los términos del servicio (y la sección voice-bot-owners, para que los propietarios acepten la versión 4 de las condiciones) · el interruptor general del operador, tras la revisión legal · el propietario del bot lo activa en @BotFather (condiciones, versión 4, propósitos) · la revisión del bot por el operador · el permiso de quien recibe la llamada (una versión de la aplicación con la pantalla de permiso) · una pasarela con las grabaciones aprobadas de los avisos de llamada saliente · un idioma del bot coreano o inglésDesactivado: a la espera de la revisión legal
Idiomas del bot distintos del coreano y el inglés (7 idiomas)Llamadas a bots de voz + una pasarela que atienda llamadas en el idioma de ese bot (desde la 0.7.0, en los 9 idiomas) · el operador leyó las instrucciones y el saludo de ese bot y lo autorizó (también en un servidor que acepta todos los bots)Desactivado: pendiente de despliegue
Pantalla de confirmación y avisos de voz en idiomas distintos del coreano y el inglés (8 idiomas)Se incluyen en las compilaciones de la aplicación y en la imagen de la pasarela a medida que cada idioma supera la revisión legal y la de hablantes nativos. Hasta entonces, quien usa la aplicación en uno de esos idiomas también ve la pantalla de confirmación en inglés y oye los avisos en inglésBorrador: pendiente de revisión
Condiciones para propietarios de bots (versión 4)Los términos del servicio publican la sección de propietarios de bots (voice-bot-owners). La transcripción, la clave propia, el modo stream, los servidores MCP y las llamadas de bots exigen haber aceptado antes estas condicionesNo se pueden aceptar
HerramientasLlamadas a bots de voz + una pasarela que admita herramientasDesactivado
Servidores MCPComponentes MCP · dirección de salida dedicada (condición de operación — el código no la comprueba) · la sección bot-calls-mcp de la política y la sección de propietarios de bots de los términos del servicio · aceptación de las condiciones para propietarios de bots · una compilación de la aplicación que muestre en la pantalla de confirmación el destinatario MCP del bot (hasta entonces se puede registrar, pero ninguna llamada lleva herramientas MCP)Desactivado
Clave de modelo propiaClave maestra del servidor · el operador la activa cuando las aplicaciones muestran la pantalla de confirmación (versión 2) con el destinatario de un bot con clave propia · condiciones para propietarios de bots · una pasarela que use las claves de los propietarios (por defecto, solo la clave de Google)Desactivado: ni siquiera se aceptan claves
TranscripciónEl operador la autoriza en el panel de administración tras una revisión legal (desactivada por defecto) · condiciones para propietarios de bots · el interruptor del propietario del bot · una pasarela con el aviso de voz de transcripción · llamada gestionadaDesactivado
Modo streamClave maestra del servidor · la sección bot-calls-stream de la política y la sección de propietarios de bots de los términos del servicio · una pasarela que acepte llamadas en modo stream · una compilación de la aplicación con la pantalla de confirmación del modo stream (todavía no existe)Desactivado
  • Quien llama tiene que tener 19 años o más; antes de llamar ve la pantalla de confirmación «An AI answers this call» («Una IA contesta esta llamada») y, al conectarse, escucha primero un aviso de voz sobre la IA (Lo que ve quien llama). Las aplicaciones sin pantalla de confirmación (incluida la aplicación original de Telegram y clientes como Telethon) no muestran el botón de llamada del bot.
  • El audio de las llamadas gestionadas va a Google: la API Gemini Live de Vertex AI, en Iowa, Estados Unidos (Google Cloud us-central1). Pabal no guarda el audio. Google no conserva el audio ni el texto después de generar la respuesta, pero el contenido que sus controles automáticos marquen como posible abuso puede conservarse hasta 90 días (lo mismo que dice la pantalla de confirmación). Es el único motor en producción, y Pabal no pasa las llamadas a otro proveedor a escondidas (no hay cambio automático).
  • A diferencia de las llamadas entre personas, las llamadas a bots no tienen cifrado de extremo a extremo: la pasarela de voz de Pabal descifra el audio para entregárselo al modelo de IA. La pantalla de confirmación se lo advierte a quien llama.

Así fluye una llamada

Quien llama Pabal (servidor del operador) Fuera de Pabal App Pabal Confirmación · 19+ Versión adaptada pabal-server MTProto · Bot API Admisión · límites Registro de llamadas Programa del bot Bot API (HTTPS) Tu código · servidor coturn Solo relé de audio pabal-voice Aviso de IA · instrucciones No guarda el audio Google Vertex AI Gemini Live us-central1 (EE. UU.) ① Llamada ② Enlace Llamadas · ajustes · herramientas ③ voice_call · herramientas Ajustes · respuestas · endVoiceCall Audio (solo relé) ④ Audio ↔ respuestas de la IA
Llamada gestionada: la señalización arriba, el audio abajo

Explicación del diagrama

  • Tres zonas: la aplicación de quien llama → el servidor del operador de Pabal (servidor de Pabal, pasarela de voz, servidor de relé) → fuera de Pabal (el modelo de IA de Google, el programa del propietario del bot). El código del propietario del bot no ve ni un byte del audio.
  • ① Llamada: la aplicación comunica al servidor el consentimiento dado en la pantalla de confirmación y después llama. El servidor comprueba tanto a quien llama (consentimiento — declara por sí mismo tener 19 años o más · compilación de la aplicación · tester · límites) como al bot (autorización · ajustes · límites), y acepta la llamada o la termina con tono de ocupado.
  • ② Enlace: el servidor pasa la llamada a la pasarela de voz junto con las instrucciones, el saludo, la voz, el idioma y las herramientas del propietario del bot. La pasarela pone siempre primero las reglas básicas de Pabal (decir que es una IA, etc.) y después las instrucciones.
  • ③ Bot API: el programa del propietario define los ajustes, recibe voice_call cuando una llamada empieza y cuando termina, y voice_call_tool_query cuando el modelo llama a una herramienta, y responde a esas consultas. La flecha azul de ③ es lo que el servidor envía al bot; la flecha gris de debajo, lo que el bot envía al servidor.
  • ④ Audio (fila de abajo): la aplicación y la pasarela no conocen la dirección de la otra y solo intercambian el audio a través del servidor de relé (coturn); la pasarela entrega el audio al modelo de Google y recibe su respuesta. El recuadro rojo marca el punto en que el audio sale de Pabal.
  • En modo stream, el servidor del propietario del bot ocupa el lugar de Google en ④ (Modo stream).

Llamadas desde el Apple Watch (en preparación)

La app del reloj no usa MTProto. El reloj inicia sesión por la API del reloj del servidor (JSON sobre HTTPS), recibe la lista de bots y la pantalla de confirmación, y llama. El servidor aplica la misma decisión de admisión que a las llamadas desde las aplicaciones y después le da al reloj un billete de un solo uso (15 segundos) válido solo para esa llamada, y pasa la llamada a la pasarela. El reloj se conecta con ese billete al punto de entrada de audio (WSS, pabal-voice-edge), que une el audio con el proceso de la pasarela que se encarga de esa llamada. No hay intercambio de claves (DH) ni relé de llamadas (coturn). Aun así, la pasarela sigue descifrando el audio para pasárselo al modelo, así que no hay cifrado de extremo a extremo y el audio va al mismo sitio. El propietario del bot recibe exactamente el mismo voice_call que en una llamada desde las aplicaciones (solo cambia el momento de started, más abajo).

Activar las llamadas de voz

Las llamadas de voz las activa una persona desde @BotFather: aceptar las reglas de los bots de voz es un compromiso del propietario del bot, no del token. Todos los demás ajustes se pueden configurar tanto en @BotFather como con la Bot API.

  1. Pide al operador que autorice tu bot. En un servidor que solo acepta bots autorizados (lo predeterminado), hasta que el operador autorice el bot con su ID (los números antes de : en el token) en la pestaña Voz del panel de administración, @BotFather no muestra el botón de llamadas de voz y cambiar los ajustes devuelve 403 VOICE_NOT_AVAILABLE. Antes de la apertura al público, pide también que añadan como tester la cuenta con la que vas a probar (tu ID de usuario es message.from.id en getUpdates después de escribirle al bot).
  2. Escribe las instrucciones. Es el texto que describe qué hace el bot al teléfono. Defínelas con setVoiceCallSettings (abajo) o envíalas con el botón Instrucciones (지시문) de la pantalla de llamadas de voz de @BotFather. Sin instrucciones no se puede activar (salvo en modo stream).
  3. En @BotFather: /mybots → tu bot → Llamadas de voz (음성 통화) → Activar (켜기). La primera vez aparecen las reglas de los bots de voz; pulsa Aceptar las reglas y activar (규칙에 동의하고 켜기) para activar. Las siguientes veces solo pregunta «¿Activar?» (켤까요?).
  4. Espera a que lo vuelvan a autorizar. En un servidor que solo acepta bots autorizados, la autorización cubre las instrucciones, el saludo y las herramientas de ese momento: si cambias las instrucciones después de la autorización, obtendrás unavailable_reason: "awaiting_review". El bot solo vuelve a recibir llamadas cuando el operador lo revisa y lo autoriza de nuevo. Pasa lo mismo cada vez que cambias cualquiera de los tres (o la dirección de stream en modo stream). Si el idioma del bot no es el coreano ni el inglés, el bot solo recibe llamadas después de que el operador lea las instrucciones y el saludo y lo autorice, incluso en un servidor que acepta todos los bots (Voces e idiomas).
  5. Comprueba que available sea true en getVoiceCallSettings y llama con el botón de llamada del perfil del bot desde una compilación de la aplicación con la pantalla de confirmación.
켜기
BotFather음성 통화를 켜기 전에 음성 봇 규칙을 확인해 주세요. 1. 봇은 언제나 AI 라고 밝혀요. 사람인 척하게 할 수 없어요. 2. 실제 기관·회사·사람을 사칭할 수 없어요. 3. 카드 번호·주민등록번호·비밀번호·인증 번호를 묻게 할 수 없어요. 4. 통화 소리는 파발이 저장하지 않고, 대답을 만들기 위해 Google(Vertex AI)로 보내져요. 5. 봇과의 음성 통화는 만 19세 이상만 할 수 있어요. 6. 규칙을 어기면 운영자가 음성 통화나 봇을 멈출 수 있어요.
규칙에 동의하고 켜기
BotFather음성 통화를 켰어요. 음성 통화 · @order_helper_bot 상태: 켜짐 목소리: Kore (단단한) · 언어: 한국어 … 전화를 받으면 먼저 "AI 음성 봇과 연결되었습니다" 안내가 나가고, 그다음 봇이 인사해요.

Por ahora, BotFather responde en coreano. Las reglas dicen: 1. el bot siempre dice que es una IA y no se le puede hacer pasar por una persona; 2. no puede suplantar a organismos, empresas ni personas reales; 3. no se le puede hacer pedir números de tarjeta, números de identificación nacional, contraseñas ni códigos de verificación; 4. Pabal no guarda el audio de las llamadas, que se envía a Google (Vertex AI) para generar las respuestas; 5. solo pueden llamar a un bot las personas de 19 años o más; 6. si se incumplen las reglas, el operador puede suspender las llamadas de voz o el bot. Tras aceptarlas, BotFather confirma que las llamadas de voz están activadas, muestra el estado (voz Kore, idioma coreano…) y recuerda que en cada llamada suena primero el aviso «You're now talking to an AI voice bot.» y después saluda el bot.

En un servidor que solo acepta bots autorizados, el estado puede aparecer así: "켜짐 — 바뀐 지시문·인사말을 운영자가 확인할 때까지 전화를 받지 않아요" («Activado: no recibe llamadas hasta que el operador revise las instrucciones y el saludo modificados») (paso 4). Si el idioma del bot no es el coreano ni el inglés, en cualquier servidor aparece así hasta que el operador lo autorice: "켜짐 — 한국어·영어가 아닌 봇은 운영자가 지시문·인사말을 읽고 허용할 때까지 전화를 받지 않아요 (자동 규칙 검사가 한국어와 영어만 읽어요)" («Activado: un bot que no está en coreano ni en inglés no recibe llamadas hasta que el operador lea las instrucciones y el saludo y lo autorice (el control automático de las reglas solo lee coreano e inglés)»).

La pantalla de llamadas de voz tiene botones para activar y desactivar, instrucciones, saludo, voz, idioma, límites, uso, transcripción, clave propia, modo stream y (en un servidor con MCP activado) servidores MCP. Aunque la voz se desactive o se retire la autorización, desactivar y borrar siempre funcionan: desactivar la voz, borrar la clave propia, salir del modo stream y eliminar un servidor MCP.

El nombre del bot también pasa el control de suplantación, al activar y en cada llamada. Un bot que usa el nombre de un organismo o una empresa real no se activa: VOICE_CONTENT_REFUSED: name: impersonation. Si el bot pertenece de verdad a ese organismo, avisa al servicio de atención del servidor.

Ajustes: setVoiceCallSettings

Define los ajustes de voz del bot. Es una modificación parcial en la que solo cambia lo que envías; el servidor lo revisa todo y lo escribe de una vez, o lo rechaza entero. Si nada cambia, no escribe nada.

curl -s "https://pabal.me/bot$BOT_TOKEN/setVoiceCallSettings" -H 'Content-Type: application/json' -d '{
  "instructions": "Eres la línea de atención telefónica de la tienda Ayudapedidos. Responde de forma breve y cortés. El horario es de 10:00 a 20:00.",
  "greeting": "Ayudapedidos, dígame. ¿En qué puedo ayudarle?",
  "voice": "kore",
  "language": "ko-KR",
  "max_call_duration": 180
}'
# {"ok":true,"result":true}
ParámetroTipoDescripción
enabledBooleanActivar o desactivar. Para activar hay que haber aceptado antes las reglas en @BotFather (si no, 403 VOICE_RULES_NOT_ACCEPTED) y, en modo gestionado, tener instrucciones. Desactivar siempre funciona: las llamadas en curso siguen hasta el final y solo dejan de aceptarse las nuevas
instructionsStringInstrucciones que sigue el modelo. Hasta 8.192 bytes en UTF-8. "" las borra; un bot gestionado activado no puede dejarlas vacías. Cuanto más largas, más cuesta la llamada, y como quien llama puede llegar a descubrirlas, no pongas en ellas contraseñas ni claves
greetingStringLo primero que dice el bot después del aviso de IA. Hasta 512 bytes. Con "", el modelo saluda brevemente por su cuenta
voiceStringID de voz (lista), kore por defecto
languageStringEl idioma en el que el bot habla y entiende: uno de los 9 ID (ko-KR por defecto, lista). zh-CN se guarda como cmn-CN, y un ID que solo difiere en mayúsculas o minúsculas (ja-jp), con la forma que tiene en la lista. Si no es coreano ni inglés, el bot solo recibe llamadas después de que el operador lea las instrucciones y el saludo y lo autorice. El aviso de voz sobre la IA del principio no usa este idioma, sino el de la pantalla de confirmación que vio quien llama
max_call_durationIntegerDuración máxima de una llamada (segundos). Mínimo 60 y máximo limits.max_call_duration (el menor entre el tope del operador y el del servidor, 600 por defecto). Valor por defecto del bot: 300
max_concurrent_callsIntegerLlamadas que atiende a la vez. Mínimo 1 y máximo limits.max_concurrent_calls (2 por defecto). Valor por defecto del bot: 2
toolsArrayHerramientas del bot que el modelo puede llamar durante la llamada (Herramientas). [] las borra todas
transcriptBooleanInterruptor de la transcripción (Transcripción)
mode, stream_url, stream_secretStringModo stream (Configurar el modo stream)
model, api_keyNo se aceptan: VOICE_FIELD_NOT_AVAILABLE. El modelo lo elige el servidor, y la clave solo se envía con setVoiceCallApiKey
  • Reglas de contenido: las instrucciones, el saludo y las descripciones de herramientas se revisan al guardar y en cada llamada. Hacer que diga que es una persona (claims_human), hacer que oculte que es una IA (hides_ai), pedirle que ignore las reglas de Pabal (overrides_rules), suplantar a organismos o empresas reales (impersonation), presentarse como organismo oficial (official_claim), pedir números de tarjeta, números de identificación nacional, contraseñas o códigos de verificación (asks_secrets), un texto que parece una clave secreta (looks_like_secret) o caracteres invisibles (hidden_text) se rechazan con VOICE_CONTENT_REFUSED: <campo>: <código>. Una frase negativa como «No pidas nunca contraseñas» sí pasa.
  • Las reglas básicas de Pabal van siempre primero. En cada llamada, el modelo lee en este orden: las reglas de Pabal → las instrucciones del propietario del bot → el cierre de las reglas de Pabal, y las instrucciones van dentro de un bloque marcado como «texto del propietario del bot». Las instrucciones no pueden sustituir ni adelantarse a las reglas de Pabal.
  • Se pueden cambiar hasta 30 veces por hora (sumando @BotFather y contando solo los cambios reales). Si te pasas: 429 Too Many Requests: retry after N y parameters.retry_after.
  • Si envías un formulario (-d name=value), pon tools como cadena JSON.
  • Un bot conectado solo por MTProto envía el mismo JSON con bots.sendCustomRequest (Bots MTProto).

Ver los ajustes actuales: getVoiceCallSettings

curl -s "https://pabal.me/bot$BOT_TOKEN/getVoiceCallSettings"
{
  "ok": true,
  "result": {
    "enabled": true,
    "available": true,
    "suspended": false,
    "rules_accepted": true,
    "instructions": "Eres la línea de atención telefónica de la tienda Ayudapedidos. …",
    "greeting": "Ayudapedidos, dígame. ¿En qué puedo ayudarle?",
    "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
  }
}
CampoSignificado
enabledSi el propietario del bot lo activó
available, unavailable_reasonSi el bot puede recibir una llamada ahora mismo, llame quien llame, y si no, por qué (tabla de abajo)
rules_accepted, owner_terms_acceptedSi se aceptaron las reglas de los bots de voz y las condiciones actuales para propietarios de bots (versión 4)
limitsLos valores máximos que se pueden fijar en este servidor y los límites diarios (minutos)
voices, languagesVoces e idiomas disponibles
testers_onlytrue si, antes de la apertura al público, solo pueden llamar los testers que elige el operador
tools, tool_limits, tool_delivery, tool_delivery_problemLas herramientas y sus límites, el camino por el que irían ahora las consultas de herramienta (webhook·get_updates·mtproto·none) y por qué no llegan (Recibir consultas de herramienta)
mode, stream_url, stream_secret_fingerprint, has_stream_secret, stream_available, stream_ignoresModo stream: el secreto no aparece nunca, solo una huella de 8 caracteres
transcript, transcript_available, transcript_unavailable_reason, transcript_delivery, transcript_delivery_problemEl interruptor de la transcripción y si de verdad se entregará
has_api_key, api_key, own_keys_availableClave de modelo propia: la clave no aparece nunca, solo el proveedor, la huella, la fecha en que se configuró y si se puede usar
mcp_available, mcp_servers, mcp_unavailable_reasonServidores MCP
outboundEl estado de las llamadas del bot (Llamadas de bots: ajustes y uso)
updated_dateÚltimo cambio (segundos Unix)
unavailable_reasonSignificado
voice_offLa voz está desactivada en este servidor, o la política de privacidad todavía no tiene la sección de llamadas a bots
kill_switchEl operador detuvo todas las llamadas de voz
bot_voice_offEl propietario del bot no lo activó
not_allowedEl operador no autorizó este bot
awaiting_reviewEl operador aún no revisó las instrucciones, el saludo o las herramientas modificados (en modo stream, la dirección de stream). Si el idioma del bot no es el coreano ni el inglés, el operador tiene que leer las instrucciones y el saludo y autorizarlo, incluso en un servidor que acepta todos los bots
suspendedEl operador suspendió la voz de este bot (o de la cuenta de su propietario)
name_refused, text_refusedEl nombre del bot, o las instrucciones, el saludo o las descripciones de herramientas guardados, no cumplen las reglas de contenido actuales. text_refused también aparece cuando el idioma elegido para el bot se retiró de la lista y ya no se puede usar: vuelve a elegir un idioma
no_relay, no_gatewayEl servidor no tiene relé de llamadas o no tiene pasarela de voz
no_gateway_with_tools, no_own_key_gatewayNo hay ninguna pasarela que pueda usar las herramientas o la clave propia de este bot
no_gateway_with_languageNo hay conectada ninguna pasarela que atienda llamadas en el idioma de este bot (Voces e idiomas)
own_key_unavailable, stream_unavailableLa clave propia o el modo stream configurados no se pueden usar ahora, así que el bot no recibe llamadas (tampoco las recibe con la clave de la plataforma en su lugar)

Voces e idiomas

Se elige entre las 30 voces prediseñadas de Gemini Live (kore por defecto). No se pueden subir ni clonar voces. Con el botón Voz (목소리) de @BotFather se elige por nombre y por carácter.

idCarácteridCarácteridCarácter
zephyrluminosapuckalegrecharonnítida
korefirmefenrirentusiastaledajuvenil
orusfirmeaoedefrescacallirrhoetranquila
autonoeluminosaenceladusaireadaiapetusclara
umbrieltranquilaalgiebasuavedespinasuave
erinomeclaraalgenibásperarasalgethinítida
laomedeiaalegreachernardelicadaalnilamfirme
schedarserenagacruxmadurapulcherrimadecidida
achirdcercanazubenelgenubirelajadavindemiatrixafectuosa
sadachbiaanimadasadaltagereruditasulafatcálida

El idioma (language) es uno de los 9 de abajo. Es el idioma en el que el bot habla y entiende, y en el que responde. En @BotFather se elige con el botón Idioma (언어) de la pantalla de llamadas de voz.

idIdiomaNotas
ko-KRCoreanoPor defecto
en-USInglés—
ja-JPJaponés—
cmn-CNChino (mandarín)Está fuera de la lista oficial de idiomas admitidos por Google, así que Google no garantiza su calidad (en las pruebas, tanto la comprensión como las respuestas fueron correctas). También se acepta zh-CN, que se guarda como cmn-CN
fr-FRFrancés—
es-USEspañol—
id-IDIndonesio—
vi-VNVietnamita—
ar-EGÁrabe (estándar)Habla en árabe estándar moderno. El código es ar-EG, el único código de árabe que tiene el modelo en tiempo real de Google, pero no fija ningún dialecto: solo habla en un dialecto si las instrucciones se lo piden
  • Un bot que no está en coreano ni en inglés solo recibe llamadas después de que el operador lea sus instrucciones y su saludo y lo autorice, incluso en un servidor que acepta todos los bots. El motivo es que el control automático de las reglas que se hace al guardar (hacerse pasar por una persona, suplantaciones, etc.) solo lee coreano e inglés. Mientras no esté autorizado, obtienes unavailable_reason: "awaiting_review", y si después de la autorización cambias las instrucciones, el saludo o las herramientas, vuelve a esperar la autorización. @BotFather lo advierte en la pantalla de elección de idioma y después de elegirlo: "한국어·영어가 아닌 언어는 자동 규칙 검사가 읽지 못해서, 운영자가 지시문·인사말을 읽고 허용한 뒤에 전화를 받아요." («Como el control automático de las reglas no lee los idiomas distintos del coreano y el inglés, el bot recibe llamadas después de que el operador lea las instrucciones y el saludo y lo autorice»). En la pestaña Voz del panel de administración, el operador ve ese bot marcado como "한국어·영어가 아닌 봇 (자동 규칙 검사가 읽지 못함): 지시문·인사말을 읽어 주세요" («Bot que no está en coreano ni en inglés (el control automático de las reglas no lo lee): lee las instrucciones y el saludo»).
  • Solo recibe llamadas si hay una pasarela que atienda llamadas en el idioma del bot. Al conectarse al servidor, cada pasarela indica en qué idiomas de bot puede atender llamadas (desde la 0.7.0, en los 9), y se considera que una pasarela que no lo indica solo atiende coreano e inglés. Si no hay ninguna pasarela que atienda llamadas en el idioma del bot, obtienes no_gateway_with_language y el botón de llamada se oculta. La llamada no se atiende en otro idioma en su lugar.
  • Los ID se aceptan sin distinguir mayúsculas y minúsculas (ja-jp) y se guardan con la forma de la lista. Un valor que no está en la lista se rechaza con 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, que indica los ID admitidos.
  • Si el idioma elegido se retira más adelante de la lista, el bot conserva sus ajustes pero deja de recibir llamadas (text_refused). Tampoco se atiende en otro idioma en su lugar, así que vuelve a elegir un idioma en Idioma (언어) de @BotFather.
  • Todavía no se puede elegir el jemer como idioma del bot. El modelo lo entiende, pero ningún hablante nativo de jemer ha revisado aún sus respuestas libres.

Si quien llama sigue hablando en otro idioma, el modelo contesta en ese idioma. El aviso de voz sobre la IA no usa el language del bot, sino el idioma de la pantalla de confirmación que vio quien llama (Lo que ve quien llama).

Recibir avisos de llamada: voice_call

Cuando alguien llama al bot, llega una actualización voice_call. Las tres actualizaciones de voz (voice_call, voice_call_tool_query, voice_call_transcript) solo llegan si las pides por su nombre: añádelas a allowed_updates de getUpdates o de setWebhook. Con una lista vacía tampoco llegan (con los demás tipos, una lista vacía hace que lleguen todos).

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": "Ana", "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": "Ana", "language_code": "ko"},
    "date": 1790380952,
    "start_date": 1790380803,
    "duration": 149,
    "max_duration": 180,
    "end_reason": "caller"
  }
}
  • direction: "incoming" para una llamada al bot, "outgoing" para una llamada que hizo el bot (actualizaciones de las llamadas de bots). Leer la falta del campo como una llamada al bot, como antes, sigue siendo correcto.
  • allowed_updates es un único ajuste por bot: se conserva la última lista que se pasó a getUpdates o setWebhook (si no das ninguna, se mantiene la anterior). Por eso, si das una lista como en el ejemplo de arriba, los tipos predeterminados que no incluyas (edited_message, callback_query) dejan de llegar: incluye todos los tipos que use tu bot. Los avisos de llamadas ocurridas antes de pedirlos no se acumulan, así que no llegan aunque los pidas después.
  • De quien llama (from) solo llegan el ID, el nombre y el idioma de la aplicación. No llegan ni el nombre de usuario ni el número de teléfono (así lo promete la pantalla de confirmación). Una cuenta eliminada aparece como first_name: "Deleted Account".
  • Con webhook llegan por POST en orden, como las demás actualizaciones. La cola de getUpdates guarda en memoria las 1.000 más recientes de cada bot y se pierde al reiniciar el servidor.
  • Un bot suspendido o eliminado no recibe nada.
eventCuándoCampos adicionales
startedCuando la aplicación de quien llama confirmó la llamada (intercambio de claves terminado) y el servidor avisó a la pasarela de que la llamada está establecida. El servidor no ve el audio: es el «establecida» que conoce el servidor. Una llamada desde el Apple Watch no tiene intercambio de claves: es cuando la pasarela avisa de que el audio del reloj está conectadomax_duration
endedCuando termina una llamada que estuvo establecida. Si espera la transcripción, puede llegar hasta 10 segundos más tarde. En un servidor con «Guardar conversación» activado, todas las llamadas que no son de modo stream llegan hasta 10 segundos más tarde, elija lo que elija quien llama, para que el propietario del bot no pueda saber qué eligióstart_date, duration (segundos que estuvo establecida, según el reloj del servidor), max_duration, end_reason, (si la transcripción estaba activada) transcript
missedUna llamada que terminó antes de establecerse (se colgó mientras sonaba, etc.)end_reason
refusedSolo cuando quien llama pasó todas las comprobaciones pero la llamada no se pudo atender por causa del bot (llamadas simultáneas, minutos diarios, pasarela); los demás rechazos no se notifican al propietario del botrefused_reason: busy (llamadas simultáneas al completo) · budget (se agotaron los minutos diarios del bot, del propietario o del servidor) · unavailable (pasarelas al completo)

end_reason: caller (colgó quien llama) · bot (colgó el bot, endVoiceCall) · max_length (duración máxima) · failed (fallo de conexión) · operator (operador o interruptor de emergencia) · suspended (bot suspendido o eliminado).

Por cada llamada que se pasó a la pasarela queda un registro de llamada (duración o «llamada cancelada») en el chat entre quien llama y el bot (no para las llamadas rechazadas antes de atenderse). La copia del bot no llega como actualización message: el registro del bot es voice_call. Si quien llama elige «Guardar conversación», el texto de la conversación queda solo en el chat de quien llama, justo debajo del registro de la llamada, y en el lado del bot no queda nada. En un servidor con «Guardar conversación» activado se reservan 30 números de mensaje después del registro de la llamada, así que el mensaje siguiente al registro tiene un número al menos 31 mayor que el del registro (también en las llamadas en las que no se guardó nada).

Durante la llamada

Colgar: endVoiceCall

curl -s "https://pabal.me/bot$BOT_TOKEN/endVoiceCall" -d voice_call_id=4611686018427387905
  • farewell (Boolean, true por defecto): si la llamada está establecida, el modelo se despide brevemente y cuelga. Si no ha colgado en 10 segundos, el servidor la termina. Con false, cuelga de inmediato. Las llamadas en modo stream siempre se cortan de inmediato.
  • El voice_call.end_reason de la llamada terminada es bot. Si no es una llamada en curso de este bot: 400 VOICE_CALL_NOT_FOUND.

Dar una nota al modelo: sendVoiceCallContext

Informa al modelo, durante la llamada, de algo que debe saber. El modelo lo lee como texto del propietario del bot, no como palabras de quien llama, y las reglas de Pabal siguen yendo primero.

curl -s "https://pabal.me/bot$BOT_TOKEN/sendVoiceCallContext" -d voice_call_id=4611686018427387905 \
  --data-urlencode "text=Esta persona acaba de cancelar el pedido A-1042. Si lo pregunta, díselo." -d respond=false
  • text: hasta 2.000 caracteres (6.144 bytes), revisado con las reglas de contenido. Con respond=true el modelo reacciona enseguida; con false (por defecto) lo usa en su siguiente turno.
  • La nota también va al modelo (a Google en una llamada gestionada), y el modelo puede decírsela a quien llama: incluye solo lo que quien llama puede saber.
  • Antes de la conexión: 400 VOICE_CALL_NOT_ACTIVE; llamada en modo stream o pasarela que no acepta notas: 400 VOICE_CONTEXT_NOT_SUPPORTED; más de 10 por minuto en una llamada: 429 (retry_after 60).

Uso: 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 (por defecto) o month. Con month se puede elegir el mes, por ejemplo month=2026-09 (un mes futuro da VOICE_USAGE_PERIOD_INVALID).
  • Los días y los meses se cuentan según la zona horaria del servidor (Asia/Seoul por defecto). remaining_seconds_today es el límite diario menos las llamadas terminadas y el tiempo que tienen reservado las llamadas en curso.
  • El registro de uso de cada llamada (sin quien llama) se guarda 180 días después de que termine el mes, y los totales mensuales se mantienen mientras exista el bot.

Herramientas: llamar al código del bot durante la llamada

Si quien llama pregunta «¿Por dónde va mi pedido A-1042?», el modelo llama a la herramienta check_order del bot, el servidor pasa esa consulta al bot y el modelo transmite de palabra la respuesta del bot. El propietario del bot define las herramientas y el código del bot las responde.

Modelo · pasarela Servidor Pabal Código del bot ① check_order {"order_id": "A-1042"} Revisa argumentos, quita extras Tarjeta o n.º ID: se para aquí ② voice_call_tool_query · budget_ms Silencio: "One moment, let me check." 5 s: "Just a moment longer, please." ③ answerVoiceCallToolQuery(result) Respuesta revisada, sin secretos Máx. 16.384 bytes ④ tool_result (solo como datos) El modelo dice el resultado Tarde: "The tool did not answer in time."
Una consulta de herramienta: modelo → servidor → bot → servidor → modelo

Explicación del diagrama

  • Tres columnas: a la izquierda, el modelo y la pasarela de voz; en el centro, el servidor de Pabal; a la derecha, el código del propietario del bot. Las líneas verticales punteadas son el tiempo. El modelo no habla directamente con el bot: todas las consultas y respuestas pasan por el servidor.
  • El recuadro gris de ①: el servidor revisa primero los argumentos: si encajan con los parameters de la herramienta, el tamaño y la profundidad, las claves no declaradas (se quitan antes de pasarla) y los valores que parecen un número de tarjeta o de identificación nacional (no llegan al bot y el modelo oye un rechazo).
  • Lo que recibe el bot en ②: el ID de la consulta, el ID de la llamada, quien llama, el nombre de la herramienta, los argumentos (una cadena JSON) y budget_ms, los milisegundos que quedan hasta que la respuesta tenga que llegar al servidor. Mientras espera, la pasarela rellena el silencio (recuadro de la izquierda).
  • La respuesta de ③ también se revisa: lo que parece un secreto, las frases que empujan a saltarse las reglas y los números de tarjeta o de identificación nacional se rechazan, y el modelo oye un error. La primera respuesta es la última: una respuesta rechazada también cierra la consulta.
  • ④ La línea azul continua es el caso normal; la roja discontinua, cuando se pasa el plazo. El modelo recibe el resultado de la herramienta solo como datos, nunca como órdenes, y si algo falla oye una frase corta de Pabal y le explica la situación a quien llama.

Definir herramientas

curl -s "https://pabal.me/bot$BOT_TOKEN/setVoiceCallSettings" -H 'Content-Type: application/json' -d '{
  "instructions": "Eres la línea de atención telefónica de la tienda Ayudapedidos. Responde de forma breve y cortés. Si la persona dice un número de pedido, busca el estado del envío con la herramienta check_order y comunícaselo.",
  "tools": [{
    "name": "check_order",
    "description": "Busca el estado del envío por número de pedido. Se llama cuando la persona dice un número de pedido.",
    "parameters": {"type": "object",
                   "properties": {"order_id": {"type": "string", "description": "Número de pedido, p. ej. A-1042"}},
                   "required": ["order_id"]},
    "timeout_ms": 5000
  }]
}'
ElementoRegla
ClavesSolo estas cuatro: name, description, parameters (opcional), timeout_ms (opcional)
Número y tamaño16 en total sumando las herramientas del bot y las herramientas MCP fijadas, y 8.192 bytes UTF-8 como suma de las definiciones (name·description·parameters). Igual para un bot con clave propia
name[A-Za-z_][A-Za-z0-9_-]{0,63}. No puede contener __ (se usa en los nombres de herramientas MCP), no puede empezar por pabal y no puede coincidir con otro sin distinguir mayúsculas y minúsculas
descriptionObligatoria, hasta 1.024 bytes, revisada con las reglas de contenido. El modelo decide con ella cuándo llamar a la herramienta, así que redáctala con precisión
parametersEn el nivel superior, {"type": "object"}. Si se omite, «sin argumentos»
timeout_msDe 1.000 a 15.000, 5.000 por defecto. Tiempo máximo que el modelo espera la respuesta

parameters solo usa una parte de JSON Schema que aceptan los dos motores.

  • Solo diez palabras clave: type, description, enum, properties, required, items, minimum, maximum, minItems, maxItems. Un único type (string·number·integer·boolean·array·object).
  • properties·required para object, items·minItems·maxItems para array (items obligatorio), minimum·maximum para números, enum solo para string (de 1 a 32 valores, 64 bytes cada uno).
  • Nombres de propiedad [A-Za-z_][A-Za-z0-9_]{0,63}, 24 propiedades por herramienta, hasta 3 niveles de object y 5 niveles de object y array combinados. Descripción de parámetro: 512 bytes.
  • Se rechazan los parámetros que piden secretos: si el nombre contiene palabras como pin·cvv·password·otp·ssn·rrn·cardnumber, o si la descripción habla de números de tarjeta, contraseñas, códigos de verificación o números de identificación nacional: asks_secrets.
  • El error señala el primer problema junto con su ruta: VOICE_TOOL_INVALID: tools[0].parameters.properties.tags: items_missing, VOICE_TOOL_INVALID: tools[0].name: reserved, VOICE_CONTENT_REFUSED: tools[0].description: impersonation.
  • Las herramientas solo se definen con la Bot API. La pantalla de llamadas de voz de @BotFather solo muestra sus nombres.
  • En un servidor que solo acepta bots autorizados, añadir o cambiar herramientas hace que el bot no reciba llamadas hasta que el operador lo vuelva a autorizar (awaiting_review, paso 4 de Activar).

Recibir consultas de herramienta

{
  "update_id": 813,
  "voice_call_tool_query": {
    "id": "8190448532776044545",
    "voice_call_id": "4611686018427387905",
    "from": {"id": 100002, "is_bot": false, "first_name": "Ana", "language_code": "ko"},
    "name": "check_order",
    "arguments": "{\"order_id\":\"A-1042\"}",
    "budget_ms": 4700
  }
}
  • arguments es el texto de un objeto JSON (4.096 bytes y profundidad 8 como máximo, ya revisado por el servidor). Aun así, son valores que el modelo sacó de las palabras de quien llama: vuelve a comprobarlos en el bot. Saber un número de pedido no debe bastar para oír el pedido de otra persona: comunica solo lo que pertenece a from.id (quien llama, verificado por Pabal).
  • budget_ms son los milisegundos desde que el bot la recibe hasta que la respuesta tiene que llegar al servidor. Es el timeout_ms de la herramienta menos el camino de vuelta al modelo (200 ms) y el camino hasta el bot (100 ms), así que suele ser unos 300 ms menor que timeout_ms. No se usan los relojes de ninguno de los dos lados.

El camino por el que la consulta llega al bot se decide en este orden (getVoiceCallSettings.tool_delivery).

  1. Por el webhook, si lo hay. voice_call_tool_query tiene que estar en el allowed_updates del webhook; si no está, no va por otro camino y no llega (tool_delivery_problem: "not_in_allowed_updates"). La consulta es un POST enviado aparte, así que no se bloquea detrás de la cola ordenada, y no se reenvía.
  2. Si no hay webhook y se llamó a getUpdates en los últimos 15 segundos, al principio del siguiente getUpdates. También tiene que estar en allowed_updates. Hasta 8 consultas en espera por bot.
  3. Si no, si el bot está conectado por MTProto, por updateBotWebhookJSONQuery (independientemente de allowed_updates).
  4. En cualquier otro caso no llega (tool_delivery_problem: "not_listening") y el modelo oye «The tool could not be reached.» («No se pudo contactar con la herramienta»).

Responder: answerVoiceCallToolQuery

ParámetroDescripción
tool_query_idEl voice_call_tool_query.id de la actualización
resultEl resultado. Texto, o un objeto o un array si el cuerpo es JSON (un texto que se lee como objeto o array JSON también se trata como JSON). Hasta 16.384 bytes UTF-8 (el operador puede bajarlo)
errorMensaje de fallo del propio bot, hasta 512 bytes. El modelo se lo transmite a quien llama
  • Exactamente uno de result y error (ninguno: TOOL_RESULT_EMPTY; los dos: TOOL_RESULT_INVALID). Devuelve True.
  • La respuesta de la herramienta va al modelo (a Google en una llamada gestionada), y el modelo le dice su contenido a quien llama: incluye solo lo que quien llama puede saber.
  • La primera respuesta es la última. Para una consulta tardía o terminada: 400 TOOL_QUERY_EXPIRED: <motivo> — answered (ya respondida), timeout, cancelled (quien llama interrumpió y el modelo la retiró), call_ended, unreachable. Un ID desconocido o de hace más de 10 minutos: TOOL_QUERY_ID_INVALID.
  • La respuesta también se revisa: se quitan los caracteres invisibles, y lo que parece un secreto (looks_like_secret), las frases que empujan a saltarse las reglas (overrides_rules·hides_ai·claims_human·asks_secrets) y los números de tarjeta o de identificación nacional (sensitive_data) dan TOOL_RESULT_REFUSED: <código>. Los datos propios del propietario, como la dirección, el teléfono o la URL de una tienda, pasan. Si se supera el tamaño: TOOL_RESULT_TOO_LONG·TOOL_ERROR_TOO_LONG.
  • Con webhook puedes responder en el cuerpo de la respuesta: basta con poner esto en la respuesta 200 al POST de la consulta de herramienta (no se ejecuta ningún otro método). Antes de responder, comprueba siempre la cabecera X-Telegram-Bot-Api-Secret-Token. Si pones datos de clientes en la respuesta sin comprobarla, cualquiera que envíe consultas falsas a esa dirección se lleva la respuesta.
{"method": "answerVoiceCallToolQuery", "tool_query_id": "8190448532776044545",
 "result": {"order_id": "A-1042", "status": "En reparto", "eta": "Llega mañana por la tarde"}}

Ejemplo: python-telegram-bot

Un bot que responde a la herramienta check_order de arriba. Recibe con getUpdates y atiende las consultas a la vez que las demás actualizaciones (concurrent_updates): si se retrasa procesando una actualización anterior, budget_ms puede agotarse. El pedido se busca por el ID de usuario de Pabal de quien lo hizo, y solo se comunica si coincide con el from.id de la consulta.

# order_voice_bot.py — pip install "python-telegram-bot==22.*"
# Ejecución: read -rs BOT_TOKEN && export BOT_TOKEN
#            CUSTOMER_ID=<ID de usuario de Pabal de quien hizo el pedido> python3 order_voice_bot.py
# Para probar, pon tu propio ID en CUSTOMER_ID: es message.from.id en getUpdates después de escribirle al 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 = {                                    # tabla pequeña del ejemplo; en la realidad, el sistema de pedidos de la tienda
    (CUSTOMER, "A-1042"): {"status": "En reparto", "eta": "Llega mañana por la tarde"},
}


def check_order(arguments, caller_id):
    """{"result": …} o {"error": …} para pasar tal cual a answerVoiceCallToolQuery"""
    order_id = str(json.loads(arguments).get("order_id", "")).strip().upper()
    if not re.fullmatch(r"[A-Z]-\d{4}", order_id):     # los argumentos los creó el modelo: se vuelven a comprobar
        return {"error": "No he entendido el número de pedido. Ejemplo: A-1042"}
    found = ORDERS.get((caller_id, order_id))          # solo los pedidos de quien llama
    if found is None:                                  # un pedido ajeno recibe la misma respuesta que uno inexistente
        return {"error": f"No encuentro el pedido {order_id}."}
    return {"result": json.dumps({"order_id": order_id, **found}, ensure_ascii=False)}


async def on_update(update: Update, context: ContextTypes.DEFAULT_TYPE):
    q = update.api_kwargs.get("voice_call_tool_query")  # PTB guarda en api_kwargs los tipos que no conoce
    if q is None:
        return
    if q["name"] == "check_order":
        answer = check_order(q["arguments"], q["from"]["id"])
    else:
        answer = {"error": "Herramienta desconocida."}
    try:
        await context.bot.do_api_request("answerVoiceCallToolQuery",
                                         api_kwargs={"tool_query_id": q["id"], **answer})
    except TelegramError as e:                         # TOOL_QUERY_EXPIRED: llegó tarde o la llamada terminó
        print("No se aceptó la respuesta:", 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)

Como aiogram se salta los tipos de actualización que no conoce, hay que recoger antes voice_call_tool_query de update.model_extra en un middleware externo del dispatcher (dp.update.outer_middleware). El repositorio de Pabal tiene un ejemplo (scripts/bots/voice_tools_aiogram.py); si lo necesitas, pídeselo al operador del servidor.

Lo que oyen el modelo y quien llama

  • Éxito: el resultado se convierte en la respuesta de función del modelo (un valor JSON si es JSON). El texto del resultado se lee solo como datos, nunca como órdenes.
  • Fallo: el modelo oye, en el idioma de la llamada, una frase corta de Pabal y le explica la situación a quien llama. Plazo superado: «The tool did not answer in time.» («La herramienta no respondió a tiempo»); sin conexión: «The tool could not be reached.» («No se pudo contactar con la herramienta»); argumento que parece un número de tarjeta: «Pabal does not pass on card numbers or national ID numbers.» («Pabal no transmite números de tarjeta ni números de identificación nacional»); y el error del bot, tal cual. Ante un argumento erróneo, el modelo recibe una pista de qué está mal y puede volver a llamar.
  • Mientras espera, la pasarela rellena el silencio: tras 1,5 segundos en silencio, «One moment, let me check.» («Un momento, lo compruebo») una vez (de inmediato si la herramienta tardó hace poco), y pasados 5 segundos, «Just a moment longer, please.» («Un momento más, por favor») una vez. Estas dos frases no suenan en el idioma de quien llama, sino en el idioma del bot, y si no hay una grabación revisada en ese idioma, suenan en inglés (por ahora, solo están revisadas las de coreano e inglés).
  • Si quien llama interrumpe (interrupción), la pasarela puede retirar las consultas abiertas, y una respuesta posterior da TOOL_QUERY_EXPIRED: cancelled.

Límites de las herramientas

Consultas abiertas a la vez: 2 por llamada y 8 por bot. Llamadas a herramientas: 30 por minuto en cada llamada y 120 por minuto en cada bot. Por encima, la consulta no llega al bot y el modelo oye enseguida too_many_open·rate_limited.

Condiciones para propietarios de bots

La transcripción, la clave de modelo propia, el modo stream, los servidores MCP y las llamadas de bots solo se pueden activar después de aceptar las condiciones para propietarios de bots (versión 4). Las acepta una persona en la pantalla de llamadas de voz de @BotFather (desde cualquiera de las pantallas de transcripción, clave propia, modo stream, servidores MCP o llamadas de bots); no se pueden aceptar con el token. Aunque hayas aceptado una versión anterior, se vuelven a pedir. Mientras los términos del servicio del servidor no publiquen la sección de propietarios de bots, el botón para aceptar no aparece (llamar antes da 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 muestra estas condiciones en coreano; el botón de abajo es Aceptar las condiciones (약관에 동의하기). En resumen, el propietario del bot se compromete a lo siguiente:

  1. Con la transcripción activada, al terminar la llamada recibes el texto de la conversación escrito por la IA; tú lo tratas y lo proteges.
  2. Cuando alguien llama a un bot con la transcripción activada, al conectarse, un aviso de voz le informa primero de que el texto de la conversación va al propietario del bot.
  3. Guardas el texto recibido solo el tiempo necesario, lo borras si quien llamó lo pide y no lo usas para entrenar modelos.
  4. Con una clave de modelo propia, el audio y el texto de las llamadas van al proveedor de tu clave, con tu contrato; ese tratamiento es tu responsabilidad y así debe constar en tu propio aviso de privacidad.
  5. En modo stream, el audio de la llamada va a tu servidor en lugar del modelo de IA de Pabal, y quien lo recibe y lo trata eres tú; usas la voz y las palabras de quien llamó solo en la medida en que la ley lo permite, y las borras cuando ya no las necesitas.
  6. Si quien llama lo pregunta, le dices dónde está tu servidor y qué hace (conviene ponerlo en la presentación del bot).
  7. El bot siempre dice que es una IA y sus respuestas las genera una IA: ninguna persona responde directamente ni se reproducen grabaciones de personas como si fueran respuestas de la IA. En modo stream, Pabal reproduce al principio de la llamada este aviso: «This is an AI voice bot. The call's audio goes to the bot developer's server.»
  8. No pides números de tarjeta, números de identificación nacional, contraseñas ni códigos de verificación, y sigues cumpliendo las reglas de los bots de voz.
  9. Pabal no guarda el texto de las conversaciones: una vez entregado, se borra del servidor como máximo 24 horas después; las claves y los secretos del modo stream y de los servidores MCP se guardan cifrados y no se registran en ninguna parte.
  10. Si registras un servidor MCP, los valores que dice quien llama van a ese servidor cuando el modelo llama a sus herramientas durante la llamada; eres responsable del tratamiento de ese servidor y de aquellos a los que lo pase, y declaras esos destinatarios con veracidad.
  11. Si incumples estas condiciones, el operador puede suspender las llamadas de voz o el bot.

Los puntos 12 a 24 son lo que cumples al activar las llamadas de bots: usarlas solo para los propósitos declarados y autorizados por el operador, sin publicidad; decir el nombre del bot y que es una IA en sus primeras palabras; no poner información sensible en context; no intentar nunca llamar a menores de 19 años; no esquivar las horas de silencio, las pausas ni los límites; usar lo que sepas de las personas llamadas solo para esos propósitos y borrarlo cuando quiten el permiso; proteger el token. Las llamadas que hace un bot no traen transcripción (punto 24). El texto de las condiciones puede cambiar tras la revisión legal.

Servidores MCP

Los servidores MCP están desactivados por defecto, y también en pabal.me

Solo se activan si el operador instala los componentes MCP (pabal-mcp, el firmante y el proxy de salida) y publica la sección bot-calls-mcp de la política de privacidad y la sección de propietarios de bots de los términos del servicio (tener una dirección de salida dedicada para las peticiones MCP es una condición de operación que el código no comprueba). Aun activados, mientras la aplicación no muestre en la pantalla de confirmación el destinatario MCP del bot, se puede registrar un servidor, pero ninguna llamada lleva herramientas MCP (mcp_unavailable_reason: "sheet"). En un servidor sin MCP, setVoiceCallMcpServer devuelve 403 VOICE_NOT_AVAILABLE: MCP tools are not available on this server y @BotFather no tiene el botón Servidores MCP (MCP 서버).

Si registras un servidor MCP que operas (o que tienes contratado), el modelo puede llamar a sus herramientas durante la llamada. Quien las llama no es el código del bot, sino Pabal: el cliente MCP de Pabal envía las peticiones a ese servidor a través del proxy de salida de Pabal y hace cumplir plazos, tamaños y revisiones. Por eso los valores que dice quien llama van a ese servidor (punto 10 de las condiciones para propietarios de bots).

Qué debe ofrecer el servidor MCP

  • Streamable HTTP, POST (versión del protocolo 2024-11-05 o posterior). https:// con certificado público, sin redirecciones y solo en los puertos que permita el servidor (443 en pabal.me).
  • Un servidor que usa credenciales debe rechazar con un 401 honesto las peticiones sin credenciales y no aceptar credenciales incorrectas. Una misma credencial solo la puede usar un bot.
  • Documento del operador: el operador del servidor publica un documento JSON (32 KB como máximo, sin redirecciones) en https://<host>/.well-known/pabal-voice-mcp con la entidad que lo opera, país, contacto, finalidad, tipos y ubicación de los datos, destinatarios y subencargados, y plazo de conservación. Es la información de destinatario que se ve en la pantalla de confirmación de quien llama; si no existe, la declara el propietario del bot al registrarlo (más abajo). Un servidor público al que cualquiera puede llamar sin credenciales (auth.type: "none") debe indicar "accepts": "any" en la entrada del endpoint de ese documento para declarar que acepta llamadas de Pabal.
  • Lo que añade Pabal: el _meta de tools/call lleva el plazo restante me.pabal/budget_ms (milisegundos) y el tamaño máximo del resultado me.pabal/result_budget_bytes, y puede llevar una aserción firmada por Pabal (me.pabal/assertion, un JWT). El emisor de la aserción, el sub (bot:<id del bot>) y las claves de verificación (JWKS) los da getVoiceCallMcpBinding.
  • tools/list ocupa como máximo 2 MB y 20 páginas. No debe haber pasos que requieran a una persona (inicio de sesión en un navegador, etc.).
  • Cada herramienta da su clasificación en _meta["me.pabal/voice"]: data (none·caller·third_party), content (system·third_party_text), effect (none·write·notify·financial), max_ms (15.000 como máximo), max_result_bytes (65.536 como máximo) y, opcionalmente, p95_ms. Si el servidor no la da, el propietario del bot la aporta al registrarlo con tools.
  • La versión 1.0 del perfil solo concede L1: solo se fijan a las llamadas las herramientas con data: "none" y effect: "none" (herramientas que solo leen información pública). Las que manejan datos de quien llama (L2) o ejecutan algo (L3) se registran pero no se usan.
  • Al registrarlo, Pabal ejecuta pruebas de conformidad (dirección, TLS, versión, 401, lista de herramientas, estructura de las herramientas, alcance de las credenciales, pasos humanos). Si alguna falla, se rechaza con MCP_CONFORMANCE_FAILED: CT-xx.

Registrar un servidor

Los secretos se envían solo en el cuerpo de la petición (en la cadena de consulta: 400). Para que no quede ningún secreto en la línea de comandos, guárdalos en un archivo y envía ese archivo.

umask 077
cat > mcp-shop.json <<'EOF'
{"label": "shop",
 "url": "https://mcp.example.com/mcp",
 "auth": {"type": "bearer", "token": "(token que te dio el servidor MCP)"}}
EOF
curl -s "https://pabal.me/bot$BOT_TOKEN/setVoiceCallMcpServer" -H 'Content-Type: application/json' -d @mcp-shop.json
  • label: [a-z][a-z0-9]{0,11}, no puede empezar por pabal. 2 servidores por bot. El nombre de herramienta que ve el modelo es label__nombredeherramienta.
  • auth.type: none, bearer (token), headers (hasta 3 cabeceras), client_credentials (token_url, client_id, client_secret, o private_key_jwt: true — antes, createVoiceCallMcpClientKey). authorization_code todavía no existe. none (servidor público) solo se registra si el documento público del operador de ese servidor declara que acepta llamadas de Pabal (si no, MCP_OPT_IN_REQUIRED).
  • Un cambio que cambia el destinatario (servidor nuevo, otra dirección u otro host de token, una declaración nueva, un nivel más alto) responde con 400 MCP_CHANGE_NEEDS_CONFIRMATION, y solo se aplica si el propietario del bot pulsa en 5 minutos el botón Confirmar (확인 — 이 변경을 적용) que aparece en @BotFather. Así, aunque se filtre el token, las palabras de quien llama no se van a escondidas a otro sitio. Una simple rotación del secreto se aplica al momento.
  • Si el operador del servidor no publica un documento, el propietario del bot declara con veracidad el destinatario con declaration (entidad que lo opera, país, contacto, finalidad, tipos de datos, ubicación, subencargados, plazo de conservación). Quien llama lo ve en la pantalla de confirmación, y si cambia se le vuelve a preguntar.
  • Registros y revisiones: 10 por hora por bot y 20 por propietario. Otros métodos: refreshVoiceCallMcpServer (volver a revisar), deleteVoiceCallMcpServer (siempre funciona), getVoiceCallMcpBinding (el emisor, el sub y el JWKS con los que tu servidor reconoce las llamadas de Pabal), createVoiceCallMcpClientKey.
  • El estado de los servidores y herramientas registrados aparece en getVoiceCallSettings.mcp_servers (sin ruta, cadena de consulta ni secretos). El texto completo del perfil (Pabal Voice MCP Profile 1.0) está en el repositorio de Pabal, en docs/app/voice-bot-mcp-profile.html; si lo necesitas, pídeselo al operador del servidor.

Clave de modelo propia

Todavía ningún servidor de producción acepta claves propias

En una llamada atendida con clave propia, quien llama tiene que ver una pantalla de confirmación (versión 2) que le diga que el audio va al proveedor de esa clave, y ninguna aplicación muestra aún esa pantalla. Por eso, un servidor que no sea de pruebas ni siquiera acepta claves hasta que el operador lo active indicando que las aplicaciones están listas (403 VOICE_OWN_KEY_NOT_AVAILABLE). El servidor también necesita una clave maestra.

Haz que las llamadas del bot usen tu propia cuenta de modelo. El coste se carga en tu cuenta del proveedor, y las llamadas con clave propia no consumen los límites diarios de la plataforma para el bot, el propietario y el servidor (los de quien llama siguen aplicándose). Antes hay que aceptar las condiciones para propietarios de bots.

curl -s "https://pabal.me/bot$BOT_TOKEN/setVoiceCallApiKey" -d provider=google \
  --data-urlencode "api_key@vertex-sa.json"
  • provider: google para una clave JSON de cuenta de servicio de Vertex AI (solo se guardan los campos necesarios) y openai para una clave de API que empieza por sk-. Cualquier otro valor: VOICE_PROVIDER_INVALID. La clave se envía solo en el cuerpo de la petición (en la cadena de consulta: 400); si la lees de un archivo como arriba, no queda en la línea de comandos. No la pegues en un chat con @BotFather.
  • Antes de aceptar la clave, el servidor comprueba con una sesión de prueba de la pasarela que funciona de verdad (12 segundos como máximo). Si pasa, la guarda y devuelve True. Si el proveedor la rechaza: API_KEY_INVALID; si inicia sesión pero no tiene permiso para la API en tiempo real: API_KEY_NO_REALTIME_ACCESS; si no hay pasarela para probar o está ocupada: 503 API_KEY_CHECK_UNAVAILABLE: …: the key was not kept.
  • Pruebas: 5 por hora por bot y 10 por propietario (sumando todos sus bots). Solo cuentan las pruebas que llegaron a una pasarela.
  • La clave no aparece en ningún sitio: ni en las respuestas, ni en los errores, ni en los logs, ni en pantalla. getVoiceCallSettings.api_key solo incluye el proveedor, la fecha en que se configuró, la huella (8 caracteres) y si se puede usar.
  • Si la clave configurada no se puede usar (cambiaron las condiciones, el servidor no puede abrir la clave, etc.), el bot no recibe llamadas: no cambia a escondidas a la clave de la plataforma. Si borras la clave con deleteVoiceCallApiKey (siempre funciona), las llamadas se atienden con la clave de la plataforma, y las que estaban en curso con tu clave terminan.
  • Las llamadas con clave propia no tienen límite diario de Pabal: solo se aplican la duración de una llamada, las llamadas simultáneas y los límites de quien llama. El coste va a tu cuenta, así que configura alertas de presupuesto en la consola del proveedor.
  • Una clave de OpenAI solo se usa si el operador de la pasarela la activa aparte (por defecto, la pasarela solo acepta claves de Google). Si no hay ninguna pasarela que la tenga activada, la prueba de una clave de OpenAI da 503 … no_gateway. El camino de las claves de OpenAI todavía no se ha probado con el OpenAI real (solo con servidores falsos), y una llamada atendida con clave de OpenAI usa la voz de OpenAI que fija la pasarela (marin por defecto), sea cual sea la voz configurada en el bot. Una clave de Google se usa en la región de Vertex de la pasarela (us-central1).
  • En modo stream, la clave solo se guarda, no se usa.

Transcripción

La transcripción solo llega en servidores donde el operador la ha autorizado

El operador tiene que autorizarla en el panel de administración después de terminar una revisión legal (desactivada por defecto). Si activas el interruptor en un servidor donde no está autorizada, obtienes transcript_available: false y transcript_unavailable_reason: "not_allowed", y no llega ningún texto.

Al activarla, el propietario del bot recibe el texto de la conversación escrito por la IA. Acepta las condiciones para propietarios de bots y después activa el interruptor.

La transcripción y «Guardar conversación» no son lo mismo

La transcripción (esta sección) la activa el propietario del bot y la recibe el propietario del bot; quien llama oye un aviso de voz al conectarse. «Guardar conversación» lo elige quien llama con la casilla de la pantalla de confirmación, y al terminar la llamada el texto queda solo en su propio chat: no va ni al bot ni a su propietario, y no hay aviso de voz. Una misma llamada puede tener las dos cosas. «Guardar conversación» no tiene ningún interruptor que active el propietario del bot: se activa cuando la política de privacidad del servidor incluye la sección bot-calls-keep-text (en preparación: Usar la aplicación: bots de voz).

curl -s "https://pabal.me/bot$BOT_TOKEN/setVoiceCallSettings" -d transcript=true
  • Las llamadas que hace un bot no tienen transcripción (llamadas de bots), aunque el interruptor esté activado.
  • Cuando alguien llama a un bot con la transcripción activada, justo después del aviso de IA oye «What is said on this call is written down and sent to the bot's owner.» («Lo que se dice en esta llamada se pone por escrito y se envía al propietario del bot»). Solo se transcribe lo que se dice después de que termine el aviso. Las llamadas en modo stream no tienen transcripción.
  • El campo transcript llega dentro del voice_call de la llamada terminada (event: "ended"). Es la única versión completa, así que incluye voice_call en allowed_updates. Si quieres recibir los turnos uno a uno durante la llamada, añade también voice_call_transcript (solo por la Bot API HTTP, no por MTProto).
  • En un turno que contiene un número de tarjeta o de identificación nacional, todos los dígitos se sustituyen por *. Un número dicho en dos turnos o leído en palabras no se detecta, así que trata con cuidado también el texto recibido.
  • 4.096 bytes por turno, 256 KiB y 4.000 turnos por llamada. Si se superan, el resto se descarta y queda truncated: true.
  • Pabal no guarda el texto de las conversaciones: está en memoria durante la llamada, se entrega una vez al bot y lo que no se recoge se borra a las 24 horas. Se pierde al reiniciar el servidor. Guardarlo y tratarlo después de recibirlo es responsabilidad del propietario del bot.
"transcript": {
  "turns": [
    {"role": "bot", "text": "Ayudapedidos, dígame. ¿En qué puedo ayudarle?", "offset_ms": 5200},
    {"role": "caller", "text": "¿Por dónde va mi pedido A-1042?", "offset_ms": 9800}
  ],
  "truncated": false
}
{"update_id": 21, "voice_call_transcript": {"voice_call_id": "4611686018427387905", "seq": 2,
 "role": "caller", "text": "¿Por dónde va mi pedido A-1042?", "is_final": true, "offset_ms": 9800, "date": 1790380813}}

role es caller (quien llama) o bot (el modelo), y offset_ms son los milisegundos desde que empezó el audio de la llamada.

Modo stream

El modo stream todavía no se puede usar

Para poder llamar a un bot en modo stream, la aplicación de quien llama tiene que mostrar la pantalla de confirmación del modo stream, que dice que «el audio de la llamada va al servidor del desarrollador del bot», y todavía no existe ninguna compilación de la aplicación que la incluya. El servidor también necesita una clave maestra, la sección bot-calls-stream de la política de privacidad, la sección de propietarios de bots y una pasarela que acepte el modo stream. Si falta algo, mode=stream devuelve 403 VOICE_STREAM_NOT_AVAILABLE.

En modo stream, el audio de la llamada no va al modelo de IA de Pabal sino al servidor del propietario del bot. Tu servidor escucha el audio en bruto y devuelve las respuestas como audio: tú eliges qué IA usar (el modelo en tiempo real de otro proveedor, tu propia combinación de reconocimiento y síntesis de voz). Pabal sigue reproduciendo el aviso de voz sobre la IA al principio y sigue aplicando la confirmación de quien llama, los límites, la duración de la llamada y el límite de llamadas simultáneas. No hacen falta instrucciones, y las herramientas, la clave propia y la transcripción solo se guardan, no se usan (stream_ignores).

Quien llama Pabal · pasarela wss del propietario ① Confirmación stream · llamada ② Upgrade + firma · pabal.voice.v1 ③ start {call_id, bot_id, format} ④ ready (en 3 s) Aviso de IA: mientras suena, la voz de quien llama no va al servidor del bot ⑤ live Audio (relé) ⑥ PCM16 24 kHz · trozos de 20 ms ⑦ clear · mark · hangup ⑧ stop {reason} → cierre 1000
Modo stream: el servidor del propietario en lugar del modelo

Explicación del diagrama

  • Tres columnas: a la izquierda, quien llama; en el centro, Pabal (el servidor y la pasarela de voz); a la derecha, el servidor del propietario del bot. El servidor del bot no habla ni con la aplicación ni con el servidor de Pabal: solo con la pasarela, por WebSocket.
  • De ② a ④ ocurren mientras suena el teléfono: la pasarela se conecta por TLS a la dirección pública que ha comprobado, hace el upgrade con la cabecera de firma y envía start. Solo se contesta la llamada si el servidor del bot envía ready en 3 segundos; si no, quien llama oye el tono de ocupado.
  • El recuadro gris es una compuerta: hasta que termina el aviso de voz sobre la IA («This is an AI voice bot. The call's audio goes to the bot developer's server.»), la voz de quien llama no va al servidor del bot, y el audio que el bot envíe antes no se reproduce. En cuanto termina, se envía ⑤ live.
  • ⑥ El audio va en los dos sentidos en PCM16, 24 kHz, mono, y del lado de quien llama siempre en trozos de 20 ms (960 bytes). Las flechas azules son lo que envía Pabal y las grises, lo que envía el servidor del bot.
  • ⑦ y ⑧: el bot puede enviar una interrupción (clear), un aviso de posición de reproducción (mark) y un colgado (hangup); cuando termina la llamada, Pabal envía stop con el motivo y cierra con 1000.

Configuración y sondeo

Primero pon en marcha el servidor de ejemplo de abajo en una dirección pública (un nombre que coincida con el certificado). El sondeo se conecta a ese servidor en el momento mismo de configurarlo, y también cuentan para el límite de 5 por hora los fallos por no poder conectar.

Después de aceptar las condiciones para propietarios de bots (@BotFather → Llamadas de voz (음성 통화) → Modo stream (원음 모드) → Aceptar las condiciones para propietarios de bots (봇 주인 약관에 동의)), indica la dirección del servidor y el secreto. El secreto tiene de 32 a 128 caracteres ASCII imprimibles (sin espacios), y una dirección nueva siempre se envía con un secreto nuevo.

umask 077
openssl rand -hex 32 | tr -d '\n' > stream-secret.txt      # no debe haber un salto de línea al final
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. Formato de la dirección: solo wss://, sin nombre de usuario, # ni espacios, solo ASCII (los demás caracteres, con codificación por porcentaje), 2.048 caracteres como máximo. El host es un nombre con al menos un punto o una IP pública (no vale localhost), y el puerto, 443 o de 1024 a 65535. Si no: 400 STREAM_URL_INVALID: ….
  2. Los límites de sondeo se cuentan en cuanto se hace la petición: 5 por hora por bot y 10 por propietario, y a la vez 1 por bot y 2 por propietario. Por encima: 429. Un sondeo que no llegó a conectarse a ningún sitio porque la pasarela estaba ocupada no cuenta.
  3. Comprobación de la dirección: si el nombre apunta aunque sea a una sola dirección privada o reservada, o a una dirección del propio Pabal: 400 STREAM_URL_INVALID: the host must resolve only to public addresses.
  4. Sondeo: la pasarela intenta conectarse una vez ("probe": true en start). Si llega ready, se activa y el secreto se guarda cifrado con la clave maestra del servidor. Si falla: 400 STREAM_PROBE_FAILED: <código> — unreachable (nombre, conexión, tiempo o TLS), handshake_failed (una respuesta distinta de 101; casi siempre se rechazó la firma), redirect, no_subprotocol, no_ready, bad_ready, address_refused, busy, url, etc.; los demás códigos llegan tal como los da la pasarela. El servidor espera hasta 12 segundos.
  5. Cuando se activa el modo stream o cambia la dirección mediante la Bot API (el token), @BotFather envía al propietario del bot un aviso de que la dirección de stream ha cambiado a … (원음 주소가 … 로 바뀌었어요 (비밀 …)) con un botón Volver al modo gestionado (관리형으로 돌아가기); cuando vuelve al modo gestionado, lo avisa con un botón Llamadas de voz (음성 통화). Es para evitar que, si se filtra el token, el audio se vaya a escondidas a otro servidor.
  • Si pones stream_secret en la cadena de consulta, se responde 400 antes que cualquier otra cosa. El modo stream no se puede configurar en una respuesta de webhook.
  • Para volver al modo gestionado: mode=managed (se borran la dirección y el secreto). Si el bot está activado y no tiene instrucciones, obtendrás VOICE_INSTRUCTIONS_REQUIRED, así que envía las instrucciones a la vez.
  • En un servidor que solo acepta bots autorizados, el bot solo recibe llamadas después de que el operador revise la dirección de stream y lo vuelva a autorizar.
  • getVoiceCallSettings solo muestra stream_url y la huella del secreto. En las llamadas en modo stream no funciona sendVoiceCallContext (VOICE_CONTEXT_NOT_SUPPORTED), y endVoiceCall cuelga al momento, sin despedida.

Especificación

  • TLS 1.2 o superior, con un certificado público que coincida con el nombre de la URL. La pasarela se conecta a la IP que ha comprobado, pero usa el nombre para el SNI y para comprobar el certificado. No sigue redirecciones (3xx).
  • Hay que elegir el subprotocolo pabal.voice.v1 (Sec-WebSocket-Protocol). Sin compresión. Un mensaje ocupa como máximo 65.536 bytes, y un mensaje de texto, como máximo 4.096 bytes (UTF-8).
  • La pasarela envía un ping cada 15 segundos. Si no hay pong en 15 segundos, corta (la mayoría de las bibliotecas responden solas).
  • Los mensajes de texto son eventos JSON y los binarios son audio. Hay un único formato de audio: PCM16 little-endian, mono, 24.000 Hz. call_id y bot_id son cadenas decimales.
SentidoMensajeCuándo · significado
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}Una vez, justo después del upgrade. No incluye el nombre, el número ni el ID de quien llama (solo caller.lang; los datos de la persona llegan con la actualización voice_call)
Pabal → bot{"event":"live"}Terminó el aviso de IA. A partir de aquí el audio circula
Pabal → botBinarioVoz de quien llama, siempre 960 bytes (20 ms). Solo después de live
Pabal → bot{"event":"mark","name":"…"}Se reprodujo todo el audio anterior a ese mark enviado por el bot
Pabal → bot{"event":"cleared","played_ms":N,"dropped_ms":N,"marks":["…"]}Respuesta a clear
Pabal → bot{"event":"buffer_full","dropped_ms":N}El audio del bot acumuló más de 30 segundos y se descartó lo que sobraba
Pabal → bot{"event":"stop","reason":"…","played_ms":N,"duration_ms":N}La llamada terminó. Enseguida se cierra con 1000
Bot → Pabal{"event":"ready"}En los 3 segundos siguientes a start, y lo primero de todo
Bot → PabalBinarioAudio del bot: número par de bytes, de 2 a 65.536. Lo que llega antes de live se descarta
Bot → Pabal{"event":"clear"}Interrupción: detener ya el audio del bot en cola
Bot → Pabal{"event":"mark","name":"[A-Za-z0-9_.:-]{1,64}"}«Avísame cuando la reproducción llegue aquí». Hasta 256 en espera por llamada
Bot → Pabal{"event":"hangup"}Termina la llamada. Antes reproduce el audio en cola durante 5 segundos como máximo y después cuelga
  • Un event desconocido se ignora. Un texto que no es JSON, un texto de más de 4 KiB y un audio con un número impar de bytes o de más de 64 KiB son una infracción de la especificación y terminan la llamada (quien llama oye un aviso de disculpa).
  • Topes de envío: eventos de texto, 50 por segundo de media (100 en ráfaga); audio, 4 veces el tiempo real de media (60 segundos en ráfaga). Si el socket del bot no lee y la voz de quien llama se acumula más de 30 segundos (1.500 trozos), la llamada también termina.

Firma

La pasarela añade esta cabecera a la petición de upgrade.

X-Pabal-Voice-Signature: t=<segundos Unix>,nonce=<16 bytes en 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 es el host de la URL que diste a Pabal (en minúsculas, sin puerto). No lo calcules a partir de la cabecera Host de la petición: ese valor lo escribe quien envía, así que no ata la firma a ningún host. path?query es la ruta y la cadena de consulta de la URL tal cual (/ si no hay). En un sondeo, call es el número del sondeo.
  • El servidor del bot comprueba obligatoriamente: |ahora − t| ≤ 300 segundos, cada nonce una sola vez y el HMAC comparado byte a byte con una función de tiempo constante. Si algo falla, responde algo distinto de 101, por ejemplo 401.
  • Y comprueba que start.call_id sea igual al call de la firma. Si no, cierra con 1008, para que un upgrade firmado no pueda reutilizarse en otra llamada. Un servidor que atiende a un solo bot comprueba también que start.bot_id sea su bot.
  • Valores de prueba: secreto example-stream-secret-of-the-bot-developer-0001, t=1790000000, nonce 00112233445566778899aabbccddeeff, call 4611686018427387905, host bot.example.com, ruta /voice?bot=100042 → v1=807517ff96ac75223bde35b48c775f49a2eda2fb4832a02c0ea832b914d70d0c.

Audio · interrupción · mark · final

  • Búfer del bot hacia quien llama: la pasarela lo envía a velocidad de reproducción, así que puedes enviarlo todo de golpe, pero si se acumulan más de 30 segundos, se descarta lo que sobra y se envía buffer_full. Una pausa de 300 ms en el audio del bot marca el final de un «fragmento» (la unidad de mark y clear).
  • clear: descarta todo el audio del bot que aún no ha salido, y el silencio empieza a partir de los siguientes 10 ms (menos de 50 ms medidos). En la respuesta cleared, played_ms es la suma del audio del bot realmente reproducido en esta llamada hasta ahora, dropped_ms lo que se acaba de descartar y marks los nombres de los mark de los fragmentos descartados; sirve para decirle al modelo «dónde se cortó».
  • mark: vuelve con el mismo nombre cuando se ha reproducido el audio anterior. Conviene que el audio anterior a un mark termine en un múltiplo de 480 bytes (10 ms); el resto se rellena con silencio.
  • Final: el bot envía hangup o cierra limpiamente con 1000 o 1001. Cualquier otro cierre o un corte de TCP termina como una infracción de la especificación (aviso de disculpa a quien llama). Cuando es Pabal quien termina, stop.reason es un código corto: hangup (colgó quien llama), max_length, bot_ended, disconnect, drain, kill_switch, operator, link_lost, media_failed… Trata cualquier valor desconocido como «terminado».
  • El start de un sondeo lleva "probe": true, un caller vacío y max_seconds: 0. Cuando envías ready, llega {"event":"stop","reason":"probe"} y se cierra. No abras nada que cueste dinero, como una sesión de modelo, en un sondeo.

Ejemplo: el servidor stream más pequeño

Un servidor que prueba toda la especificación sin IA: con live reproduce un aviso sonoro y después devuelve tal cual la voz de quien llama (eco). Comprueba la firma, la hora, el nonce, la llamada y el bot. Comprueba primero con él que una llamada llega hasta el final y después pon tu IA en el lugar del eco. En el repositorio de Pabal también están scripts/bots/stream_echo_bot.py (con una demostración de la interrupción) y scripts/bots/stream_openai_relay.mjs (retransmite a Realtime con tu clave de OpenAI; es un ejemplo que no se ha ejecutado); si los necesitas, pídeselos al operador del servidor.

# stream_bot.py — pip install "websockets>=13"
# Ejecución: STREAM_SECRET_FILE=stream-secret.txt TLS_CERT=fullchain.pem TLS_KEY=privkey.pem \
#            PUBLIC_HOST=bot.example.com BOT_ID=<ID del bot: los números antes de : en el 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()   # host del stream_url que diste a Pabal (no la cabecera Host)
BOT_ID = os.environ.get("BOT_ID", "")             # vacío: acepta llamadas de cualquier 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 durante 0,6 s, PCM16 24 kHz
seen = {}                                         # nonces ya usados


def signed_call(header, target):
    """Comprueba X-Pabal-Voice-Signature y devuelve el id de llamada firmado. None si falla."""
    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 a 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 tiene que hablar de esta llamada
    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"}))    # en 3 segundos, lo primero de todo
    live = False
    async for message in ws:
        if isinstance(message, bytes):
            if live:
                await ws.send(message)            # devuelve tal cual los 20 ms de quien llama (eco)
            continue
        event = json.loads(message)
        if event.get("event") == "live" and not start.get("probe"):   # terminó el aviso de IA
            live = True
            await ws.send(CHIME)
            await ws.send(json.dumps({"event": "mark", "name": "chime"}))
        elif event.get("event") == "stop":
            print("Fin:", 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"Escuchando: wss://{PUBLIC_HOST}:{PORT}{PATH}", flush=True)
        await asyncio.get_running_loop().create_future()


asyncio.run(main())

Lo que ve quien llama

  • Botón de llamada: aparece un botón de llamada en la cabecera del chat y en el perfil del bot de voz. Solo se ve si el servidor considera que puede atender una llamada de esa persona desde ese dispositivo (antes de la apertura al público, solo para los testers). No hay videollamadas.
  • Pantalla de confirmación (antes incluso del permiso del micrófono): título «An AI answers this call»; que el bot no es una persona y que su voz la genera una IA; que no hay cifrado de extremo a extremo; adónde va el audio (Google, la API Gemini Live de Vertex AI) y en qué país o región («United States (Iowa, Google Cloud us-central1)»); qué se envía (la voz durante la llamada, el texto de la conversación y las respuestas de la IA, las notas que el bot añade durante la llamada y los resultados de las funciones del bot); la conservación (Pabal no guarda el audio y solo conserva el registro de llamadas durante 180 días; Google no guarda nada después de generar la respuesta, pero el contenido que sus controles automáticos marquen como posible abuso puede conservarse hasta 90 días); lo que recibe el propietario del bot (nombre, número de cuenta, idioma, hora y duración de la llamada, valores usados por las herramientas; ni el número de teléfono ni el audio); y los 19 años o más. Los botones son «I'm 19 or older · Call» y Cancelar. La edad no es una verificación de identidad: la confirma la propia persona. El botón de llamar es el botón principal (relleno, color de acento) y Cancelar el secundario. En un servidor con «Guardar conversación» activado, después de las explicaciones aparece la casilla «Keep this call's conversation as text in my chat» (desmarcada por defecto).
  • Con qué frecuencia: en escritorio y Android, la primera vez que se llama a ese bot (y otra vez si cambian el destinatario o la versión de la pantalla); en iPhone y Apple Watch, siempre. El reloj muestra siempre la pantalla completa, y el valor inicial del interruptor de «Guardar conversación» es lo que se eligió en la última llamada a ese bot. La aplicación envía el consentimiento al servidor en cada llamada, y el servidor usa una sola vez, para esa llamada, un consentimiento del mismo dispositivo, para el mismo bot y de menos de 2 minutos.
  • Al conectarse: el aviso de voz «You're now talking to an AI voice bot.» (en un bot en modo stream, «This is an AI voice bot. The call's audio goes to the bot developer's server.») → (en un bot con transcripción) el aviso de transcripción → el saludo del bot. El propietario del bot no puede quitar el aviso, y lo que dice quien llama antes de que termine no llega al modelo. «Guardar conversación» no tiene aviso de voz, porque quien llama lo eligió directamente en la pantalla.
  • Idioma de los avisos: los avisos de voz no usan el language del bot, sino el idioma de la pantalla de confirmación que leyó quien llama. Todos los avisos de una llamada van en el mismo idioma: el aviso de IA, el aviso de transcripción y la disculpa si se corta la conexión (en un bot en modo stream, el aviso del modo stream y la disculpa). Si a cualquiera de ellos le falta la grabación revisada en ese idioma, todos suenan en inglés. Por ahora, los idiomas revisados son solo el coreano y el inglés. La pantalla de confirmación y los avisos de los otros 8 idiomas (japonés, chino, francés, español, indonesio, vietnamita, jemer y árabe) son borradores pendientes de la revisión legal y de la revisión de hablantes nativos, así que por ahora quien usa la aplicación en esos idiomas también ve la pantalla de confirmación en inglés y oye los avisos en inglés. Las grabaciones en jemer y la versión japonesa de «Just a moment longer, please.» aún esperan a que una persona las escuche y elija. Antes del primer aviso de la llamada y del aviso de transcripción siempre hay al menos 300 ms de silencio, para que la línea telefónica no se trague la primera palabra.
  • Pantalla de llamada: bajo el nombre, «@{username} · AI · Bot» y «AI-generated voice · Call audio is processed by Google». No hay clave de emojis ni texto sobre cifrado.
  • Si hay un problema de conexión, se oye «Sorry, we've lost the connection. Ending the call now.» y la llamada se corta. Todos los rechazos terminan con tono de ocupado.

La explicación para quienes usan la aplicación está en Usar la aplicación — Llamar a un bot de voz.

Llamadas de bots

Desactivado en pabal.me: a la espera de la revisión legal

El servidor ya lo tiene, pero ningún bot puede llamar a nadie hasta que la sección bot-calls-outbound de la política de privacidad y la sección voice-bot-owners-outbound de los términos del servicio hayan pasado la revisión legal y estén publicadas. Tampoco ha salido todavía ninguna versión de la aplicación con la pantalla de permiso (Estado actual). Lo que sigue describe cómo funciona en un servidor donde está activado.

Un bot de voz también puede llamar primero a una persona: para confirmar una cita, despertarla a la hora que eligió o avisar de una entrega. El código del propietario del bot lo pide con startVoiceCall, el servidor evalúa la petición y hace sonar los dispositivos de la persona y, si contesta, primero suena el aviso de Pabal y después el modelo de voz con IA habla según las instrucciones del bot. Cómo terminó llega en una actualización voice_call.

  • Solo a quien lo ha permitido. La persona tiene que permitir en la aplicación Pabal que ese bot la llame (El permiso de quien recibe la llamada) y puede quitarlo cuando quiera.
  • No es para publicidad. Hay cuatro propósitos —recordatorios, despertador, entregas y citas— y no se permiten la publicidad, la venta ni la promoción (condiciones para propietarios de bots, puntos 12 a 24).
  • Las llamadas de bots no tienen cifrado de extremo a extremo: el audio pasa por el servidor de Pabal y por el proveedor de IA. Cuando la persona contesta, el audio va a Google durante la llamada: la API Gemini Live de Vertex AI, en Iowa, Estados Unidos (Google Cloud us-central1). Pabal no guarda el audio.
  • Al propietario del bot no le llega el texto de la conversación. Las llamadas que hace un bot no tienen transcripción. Lo que recibe el propietario del bot son las cinco cosas de Lo que ve y oye quien recibe la llamada.

Cómo funciona

El código del bot Servidor de Pabal Pasarela · modelo La app de la persona ① startVoiceCall Evaluación: bot · permiso · límites horas de silencio · pausa ② 200 {voice_call_id} ③ dial ④ Timbre: nombre (AI) ⑤ Contesta ⑥ Aviso de Pabal ⑦ Bot: nombre · IA · propósito La llamada: el audio pasa por la pasarela y el modelo de IA ⑧ voice_call: started · ended Sin respuesta · sin timbre: not_answered
Una llamada de un bot: petición → evaluación → timbre → respuesta → aviso → primeras palabras → fin

Explicación del diagrama

  • Cuatro carriles: de izquierda a derecha, el código del propietario del bot, el servidor de Pabal, la pasarela de voz con el modelo de IA y la aplicación de quien recibe la llamada. Las líneas verticales punteadas son el tiempo; las líneas azules continuas, la llamada que avanza; la línea gris, la respuesta al bot.
  • El recuadro gris de ①: tres evaluaciones. Antes de responder, el lado del bot (si lo activó, la revisión del operador, las condiciones, los límites) y si hay permiso; después del 200, el lado de la persona (horas de silencio, pausa, límite diario e intervalo de la persona, un dispositivo que hacer sonar); y una vez más justo antes del timbre.
  • El 200 de ② solo significa «recibida». Si sonó y si contestó solo lo dicen las actualizaciones de ⑧.
  • ④ a ⑦, el lado de la persona: el timbre muestra «(AI)» tras el nombre del bot y, al contestar, el aviso de Pabal (⑥) suena antes que el bot. En sus primeras palabras (⑦) el bot dice su nombre, que es una IA y el propósito de la llamada. Durante el recuadro verde el audio pasa por la pasarela y el modelo de IA: no tiene cifrado de extremo a extremo.
  • La línea roja punteada es la clave: una petición que nunca sonó por el lado de la persona y otra que sonó sin respuesta terminan igual, a la misma hora, con not_answered. El bot no puede saber si la persona dormía o estaba en otra llamada.

Activarlo — @BotFather

Las llamadas de un bot las activa solo su propietario en @BotFather, nunca el token (enviar campos de llamadas a setVoiceCallSettings da 400 VOICE_FIELD_NOT_AVAILABLE). Primero tienen que estar activadas las llamadas de voz. /mybots → el bot → Llamadas de voz (음성 통화) → Llamadas del bot (봇이 거는 전화) → Activar (켜기).

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

BotFather responde en coreano. El primer mensaje pregunta si el bot puede llamar a personas: solo a quienes lo permitieron, nunca en sus horas de silencio ni durante una pausa, con un tope diario (ahora 50) y los límites del servidor, y solo después de que el operador revise y autorice el nombre, la foto, las instrucciones, la voz y los propósitos del bot (y otra vez cada vez que cambie alguno); las llamadas de bots no tienen cifrado de extremo a extremo. El segundo muestra la pantalla del bot una vez activado: activado, esperando al operador, propósito entregas, tope 50, peticiones de hoy.

  • Condiciones, versión 4: si aún no las aceptaste, antes de activarlo aparecen las condiciones para propietarios de bots. Los puntos 12 a 24 son los compromisos de las llamadas de bots.
  • Propósitos: solo se activa con al menos uno de reminder (recordatorios) · wake_up (despertador) · delivery (entregas) · appointment (citas). Cada petición indica uno de ellos; un propósito que el bot no eligió da 400. Cambiar los propósitos vuelve a pedir permiso a quienes lo dieron, y el operador vuelve a revisar.
  • Tope diario: cuántas peticiones puede enviar el bot al día: 10, 20 o 50 (50 por defecto).
  • La revisión del operador: el operador mira en el panel de administración el nombre, el @nombre de usuario, la foto, las instrucciones, el saludo, las herramientas, la voz, el idioma, el modo, los propósitos y el nombre del propietario, y lo autoriza. Si cambia cualquiera de ellos, el bot no puede llamar hasta que lo autoricen de nuevo (403 VOICE_OUTBOUND_OFF: review).
  • El token: cambiar los ajustes de voz con el token (salvo apagar la voz) congela las llamadas del bot durante 24 horas (frozen). Si fuiste tú, levántalo con Lo cambié yo (내가 바꿨어요, vdk·vdK) en @BotFather. Volver a emitir el token también lo congela 24 horas, termina sus llamadas en curso y borra el webhook de un bot con las llamadas activadas.
  • Solo pueden llamar los bots en coreano o inglés (ko-KR·en-US). Un bot en modo stream, con clave propia, herramientas o servidores MCP no puede (mode).

El permiso de quien recibe la llamada

Un bot no puede conseguir el permiso por sí mismo. La persona abre la pantalla de permiso de ese bot en la aplicación Pabal y pulsa «I am 19 or older · Allow» (Tengo 19 años o más · Permitir). La pantalla dice que una IA hace las llamadas, los propósitos, el nombre del propietario del bot, adónde va el audio y las horas de silencio (Usar la aplicación: permitir que un bot te llame). Se abre de tres maneras.

  • El menú del bot: «Allow calls…» (Permitir llamadas…) en el chat o el perfil del bot.
  • Un enlace en el chat uno a uno: el bot envía https://pabal.me/<nombre de usuario del bot>?callme=1 a su chat uno a uno con la persona, como enlace en el texto o botón de URL en línea. La pantalla solo se abre con un mensaje que ese bot envió en ese chat: un enlace reenviado a otro chat, una vista previa o la web solo abren el chat. Una persona puede abrir la pantalla desde un enlace una vez al día (sumando todos los bots), y nunca si jamás le escribió al bot.
  • La lista de ajustes «Bots allowed to call me» (Bots que pueden llamarme): ver los bots permitidos, quitarles el permiso y volver a dárselo.
curl -s "https://pabal.me/bot$BOT_TOKEN/sendMessage" -d chat_id=$USER_ID \
  --data-urlencode "text=¿Quiere que le llamemos antes de que llegue su pedido?" \
  --data-urlencode 'reply_markup={"inline_keyboard": [[{"text": "Permitir llamadas", "url": "https://pabal.me/order_helper_bot?callme=1"}]]}'
  • Servidores propios: las aplicaciones solo conocen los enlaces de pabal.me. En otro servidor, hay que añadir su host a las reglas de enlaces de la versión de la aplicación para que un enlace abra la pantalla (el menú y la lista de ajustes funcionan igual).
  • Por ahora: solo cuentas con número +82, con el dispositivo en la zona horaria de Corea (Seúl) y con al menos 24 horas de sesión en ese dispositivo pueden activar el permiso (el operador decide los países). Quien responde que tiene menos de 19 años no puede hablar con bots durante un año.
  • Si cambian el nombre, el propietario o los propósitos del bot, o pasa un año desde el permiso, se vuelve a preguntar; mientras tanto el bot no puede llamar.

Llamar — startVoiceCall

curl -s "https://pabal.me/bot$BOT_TOKEN/startVoiceCall" -d user_id=$USER_ID -d purpose=delivery \
  --data-urlencode "context=El pedido A-1042 llega hoy hacia las 15:00. Pregúntele si podrá recibirlo entonces." \
  -d max_duration=180 -d ring_timeout=30
{"ok": true, "result": {"voice_call_id": "8812345678901234"}}
ParámetroSignificado
user_idEl id de la persona a la que llamar, que tiene que haber permitido este bot
purposeUno de los propósitos que eligió este bot: reminder · wake_up · delivery · appointment
contextLo que se le dice al modelo, y solo a él, sobre esta llamada: de 1 a 1.000 caracteres, 3.072 bytes. Se comprueba con las reglas de contenido (suplantación, ocultar la IA, pedir secretos, publicidad). El modelo lo usa tras sus primeras palabras (nombre, IA, propósito), cuando la persona ya contestó
max_durationLo más que puede durar la llamada una vez contestada (segundos): de 30 hasta 300 o el máximo del bot, lo que sea menor. 180 por defecto
ring_timeoutCuánto suena (segundos), de 30 a 45. 40 por defecto
RespuestaSignificadoQué hacer
200 {"voice_call_id": "…"}Recibida. Si sonó y si contestaron llega en las actualizaciones—
400 Bad Request: <qué>Un parámetro erróneo, un propósito que el bot no eligió (purpose is not declared), un idioma del bot distinto del coreano o el inglésCorregirlo
400 VOICE_CONTEXT_REFUSED: <código>context incumple las reglas de contenidoCorregirlo
403 VOICE_OUTBOUND_OFF: <motivo>Lado del bot: owner_off (no activado) · terms (condiciones, versión 4) · review (esperando la revisión del operador) · suspended (parada automática) · frozen (congelado por el token) · owner_name (el nombre del propietario no pasa el control de suplantación) · mode, o un motivo del lado del bot de los ajustes de voz (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. Los motivos del lado del servidor voice_off · kill_switch · no_relay · outbound_off llegan como 503, no como 403@BotFather · el operador
403 USER_UNAVAILABLENo se puede llamar a esta persona: sin permiso, quitado o pendiente de renovar, bloqueado, no es una persona, fuera de alcance, menor de 19, no es probador. El cuerpo es siempre el mismo, sea cual sea el motivoPedir el permiso
429 retry after NUn límite del lado del bot o del propietario: ritmo de peticiones, peticiones diarias del bot o del propietario, llamadas simultáneas del bot, minutos salientes del propietario, minutos de llamada al día del bot o del propietario (compartidos con las llamadas al bot)Tras N segundos
503 VOICE_UNAVAILABLELado del servidor: el interruptor de llamadas salientes está apagado, la voz está apagada en el servidor (también cuando la política de privacidad no tiene la sección de llamadas de bots), el interruptor de emergencia, no hay relé de llamadas, los límites y la cuota saliente del servidor, ninguna pasarela que pueda llamar (o todas llenas), no se pudo registrar la peticiónMás tarde
{"ok": false, "error_code": 403, "description": "Forbidden: USER_UNAVAILABLE"}

El lado de la persona se evalúa después del 200. Aunque haya permiso, en las horas de silencio de la persona, durante una pausa, superado el límite diario con esa persona o su propio límite diario e intervalo, sin dispositivo que hacer sonar o si está en otra llamada, no suena nada y not_answered llega unos ring_timeout + 12 segundos después de la petición, igual que un timbre que nadie contestó. El permiso no cambia el lado del bot (403, 429, 503).

Ajustes y uso

"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 en getVoiceCallSettings siempre está: lo que se ajustó en @BotFather (enabled·purposes·daily_cap), la revisión del operador (reviewed), cuándo termina un congelamiento por token (frozen_until, 0 si no está congelado), la parada automática (suspended) y si el bot puede llamar ahora (available). unavailable_reason solo aparece cuando available es false (del lado del bot, los mismos valores que los motivos de 403 VOICE_OUTBOUND_OFF; del lado del servidor, voice_off · kill_switch · no_relay · outbound_off: estos cuatro corresponden a 503 VOICE_UNAVAILABLE).
  • outbound en getVoiceCallUsage solo aparece en un bot que alguna vez activó las llamadas: peticiones, contestadas, sin contestar y minutos de hoy, y los límites diarios que se le aplican. No se da cuántas sonaron. calls·seconds·missed·refused nunca cuentan las llamadas que hizo el bot. Pero las llamadas que hace el bot gastan los mismos minutos de llamada al día del bot y de su propietario que las llamadas al bot: se cobran los segundos de una llamada contestada, así que bot y owner de remaining_seconds_today bajan; mientras suena, ya bajan lo que la llamada reserva, y eso vuelve si nadie contesta.
  • setVoiceCallSettings no puede cambiarlos (400 VOICE_FIELD_NOT_AVAILABLE).

Actualizaciones

Llegan como las mismas actualizaciones voice_call de las llamadas al bot (pon voice_call en allowed_updates). direction es "outgoing" y la persona solo aparece como el id de to (sin nombre, nombre de usuario ni número de teléfono).

{
  "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
  }
}
eventCuándoCampos extra
startedLa persona contestó y la llamada se conectó. En un Apple Watch, cuando se unió el audio del reloj (si nunca lo hace, un ended de 0 segundos con failed)max_duration
endedTerminó una llamada contestada. Llega hasta 10 segundos más tarde sea cual sea la configuración del servidor, mientras el servidor espera el uso de la pasarelastart_date, duration, max_duration, end_reason
not_answeredUna petición que nadie contestó, sin distinguir si sonó sin respuesta, si la rechazaron o si nunca sonó por el lado de la persona. Unos ring_timeout + 12 segundos después de la peticiónNinguno
failedSe perdió el enlace con la pasarela que tenía la petición antes de que contestaranNinguno

end_reason: callee (colgó la persona) · bot (colgó el bot, o se volvió a emitir su token) · max_length (duración máxima) · failed (falló la conexión tras contestar) · operator (el operador o el interruptor de emergencia) · suspended (bot o propietario suspendido). Las llamadas que hace un bot no tienen missed, refused ni ringing —lo rápido que contesta alguien delataría si estaba junto al teléfono— y tampoco transcript (desactivado por diseño).

Durante la llamada

  • endVoiceCall: antes de que se avise al bot de cómo terminó, retira la petición, esté sonando o todavía no (true, y luego not_answered). Termina una llamada contestada (end_reason bot). Retirar una petición en los primeros 15 segundos pone la pareja en pausa una hora.
  • sendVoiceCallContext: solo después de que la persona contestó (antes, VOICE_CALL_NOT_FOUND). Tres por llamada, 1.000 caracteres y 3.072 bytes, las mismas reglas de contenido que context. Si la pasarela no acepta notas, VOICE_CONTEXT_NOT_SUPPORTED.

Lo que ve y oye quien recibe la llamada

  • El timbre: «(AI)» tras el nombre del bot y «AI voice bot · Pabal never asks for codes» (bot de voz con IA · Pabal nunca pide códigos).
  • Al contestar suena un aviso antes que el 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.» (Es una llamada de voz con IA de Pabal. Nunca pedimos contraseñas, códigos ni transferencias. Para dejar de recibir estas llamadas, quita el permiso de llamadas en el chat del bot), en el idioma de la pantalla de permiso que leyó la persona (coreano o inglés); desde una aplicación en otro idioma, en inglés. El propietario del bot no puede quitarlo.
  • Las primeras palabras del bot: su nombre, que es una IA y el propósito de la llamada. context va después.
  • Cómo quitarlo: «Stop calls from this bot» (Dejar de recibir llamadas de este bot) en el menú, bloquear el bot o denunciarlo lo quitan siempre. Decir claramente durante una llamada algo como «no me vuelvas a llamar» lo quita en el acto (un simple «basta» depende del criterio del modelo).
  • En pausa: tras un rechazo, un timbre sin respuesta o colgar en los 10 segundos siguientes a contestar, el bot no puede llamar a esa persona durante 24 horas.
  • El registro de llamada perdida: cuando un dispositivo sonó de verdad, la persona no bloqueó el bot y el chat del bot no tiene mensajes sin leer, queda «Missed AI bot call» (llamada perdida de un bot con IA, leída, sin notificación). Si hay un mensaje sin leer, o si no sonó por las horas de silencio o una pausa, no queda registro. Todas las peticiones de un bot se guardan sin excepción en las pruebas de denuncia (30 días).
  • Lo que recibe el propietario del bot (en el orden de la pantalla de permiso):
    1. el id de Pabal de la persona
    2. si el permiso está activo (lo muestran las respuestas a sus peticiones de llamada)
    3. si la persona contestó
    4. cuándo empezó la llamada y cuánto duró
    5. quién colgó
    Una llamada perdida y una rechazada se ven igual. Lo que se dijo en la llamada no se envía.
  • Dispositivos compartidos: cualquier dispositivo con la sesión iniciada puede contestar, y el bot le dice el propósito y lo que se le indicó a quien conteste. En un ordenador compartido, recuerda cerrar la sesión.
  • La pantalla de bloqueo: en Android 14 y posteriores, si la persona no permite las notificaciones a pantalla completa, la llamada llega como notificación y no como pantalla de llamada sobre la pantalla de bloqueo. Los iPhone y los Apple Watch suenan como una llamada telefónica.
  • Guardar conversación la guarda solo en el chat de la persona, si lo elige en la pantalla de permiso. Con Contestar en el reloj activado en la app Pabal para Apple Watch, el Apple Watch también suena.
  • Las llamadas de bots no tienen cifrado de extremo a extremo: el audio pasa por el servidor de Pabal y por el proveedor de IA.

Límites · horas de silencio · pausa · parada automática

LímitePor defectoEn un servidor de producción
Un bot a una persona (al día)3 llamadasEl operador solo puede bajarlo
A una persona (al día, todos los bots)6 llamadasSolo se puede bajar
Intervalo entre llamadas a una persona10 minutosSolo se puede subir
Peticiones de un bot (al día)50 llamadasEl propietario elige 10, 20 o 50 en @BotFather
Un propietario de bots (al día, todos sus bots)100 llamadas · 30 minutosLas llamadas solo se pueden bajar
Hasta que el bot pueda enviar su siguiente petición10 segundosSolo se puede subir
Minutos salientes del servidorel 30 % de los minutos diarios del servidorSolo se puede bajar
Horas de silencio21:00–08:00El operador y la persona solo pueden ampliarlas. En la zona horaria del dispositivo
Franja de despertador (solo wake_up)como mucho 120 minutosLa persona la elige dentro de sus horas de silencio
Timbre (ring_timeout)30–45 segundos40 segundos si la petición no lo indica
Pausa: un rechazo, un timbre sin respuesta, colgar en los 10 segundos siguientes a contestar24 horasSolo se puede subir
Pausa: el bot retiró la petición en los primeros 15 segundos1 horaSolo se puede subir
  • «Al día» es un día en la zona horaria del servidor (TELEGRAM_VOICE_TIME_ZONE). Una llamada nunca dura más de 300 segundos.
  • Las llamadas de bots también gastan los mismos minutos de llamada al día que las llamadas al bot (60 minutos del bot · 120 minutos del propietario). Cuando se agotan, startVoiceCall recibe 429 y las llamadas al bot también se rechazan (refused, budget).
  • Una pausa termina cuando pasa el tiempo, cuando la persona llama al bot, pulsa «I can take calls now» (Ya puedo recibir llamadas) o lo permite de nuevo. Un mensaje o un botón del bot no la termina.
  • La parada automática solo detiene las llamadas del bot a personas (403 VOICE_OUTBOUND_OFF: suspended) y dura hasta que el operador lo comprueba y la levanta: una tasa de respuesta inferior al 20 % en las últimas 20 llamadas, 3 denuncias en 7 días de personas a las que hizo sonar, un 30 % o más de sus personas quitando el permiso (con al menos 10), 2 llamadas cortadas por las reglas durante la llamada, 5 retiradas en los primeros 15 segundos, o 2 personas o menos contestando con 60 minutos o más contestados.

Límites

LímitePor defectoQuién lo fija
Duración máxima de una llamada300 s para el bot (rango configurable: de 60 al tope del operador y del servidor, 600 por defecto)El propietario del bot, dentro del tope del operador y del servidor. Los botones de @BotFather ofrecen 1, 3, 5 y 10 minutos
Llamadas simultáneas de un bot2 (de 1 al tope del operador y del servidor, 2 por defecto)El propietario del bot, dentro del tope del operador y del servidor
Minutos de llamada al día de un bot60 minutosEl operador
Al día por propietario de bot (todos sus bots)120 minutosEl operador
Al día para todo el servidor300 minutosEl operador
Por persona que llama60 minutos al día, 20 llamadas por horaEl operador
Llamada más corta que se atiende60 segundos: si queda menos tiempo, no se atiendeEl operador
Cuentas nuevasQuien se registró hace menos de 7 días no puede llamar, salvo los testers y las personas a las que el bot envió un mensaje hace más de un díaEl operador
Cambiar los ajustes30 por hora por bot—
Notas durante la llamada10 por minuto por llamada—
Herramientas16 herramientas y 8 KB; consultas abiertas: 2 por llamada y 8 por bot; por minuto: 30 por llamada y 120 por bot—
Pruebas de clave propia · sondeos stream5 por hora por bot y 10 por propietario—
Registros y revisiones MCP10 por hora por bot, 20 por propietario, 2 servidores por bot—

Desde el Apple Watch no se puede llamar a bots que usan herramientas, servidores MCP, clave propia, modo stream o la transcripción del propietario del bot. Este rechazo solo llega a quien llama (403 WATCH_NOT_YET en la app del reloj), y el bot no recibe ninguna actualización refused.

La duración de una llamada es el menor de estos valores: el tiempo que le queda hoy a quien llama, al bot, al propietario y al servidor, y el tope de la llamada. Al atenderla, esa duración se reserva en los cuatro límites, y al terminar solo se cobran los segundos reales según el reloj del servidor. Las llamadas con clave propia no consumen los límites del bot, del propietario ni del servidor (los de quien llama siguen aplicándose).

Errores

Los errores tienen la misma forma que en los demás métodos: {"ok": false, "error_code": 400, "description": "Bad Request: VOICE_…"}. Un 403 tiene la forma Forbidden: <código>: <descripción> (salvo USER_UNAVAILABLE, que siempre es exactamente Forbidden: USER_UNAVAILABLE, sin descripción), y un 429 lleva parameters.retry_after.

CódigoSignificado
403 VOICE_RULES_NOT_ACCEPTEDPrimero hay que aceptar las reglas de los bots de voz en @BotFather
403 VOICE_NOT_AVAILABLEEste bot no puede usar la voz en este servidor (voz desactivada, el operador no lo autorizó): no se pueden cambiar los ajustes, solo desactivar y borrar
403 VOICE_SUSPENDEDEl operador suspendió la voz de este bot. Solo se puede desactivar
403 VOICE_OWNER_TERMS_NOT_ACCEPTEDPrimero hay que aceptar las condiciones para propietarios de bots (versión 4)
403 VOICE_OWN_KEY_NOT_AVAILABLEEste servidor no acepta claves propias
403 VOICE_STREAM_NOT_AVAILABLEEl modo stream no se puede usar ahora en este servidor
403 VOICE_OUTBOUND_OFF: <motivo>Este bot no puede llamar a personas ahora: owner_off · terms · review · suspended · frozen · owner_name · mode, o un motivo del lado del bot de los ajustes de voz (Llamar). voice_off · kill_switch · no_relay · outbound_off llegan como 503
403 USER_UNAVAILABLENo se puede llamar a esta persona: siempre el mismo cuerpo sea cual sea el motivo, incluida la falta de permiso
503 VOICE_UNAVAILABLEEl servidor no puede llamar a personas ahora (interruptor de llamadas salientes, voz apagada en el servidor, interruptor de emergencia, sin relé de llamadas, límites del servidor, pasarelas, petición sin registrar): inténtalo más tarde
Bad Request: purpose is not declared, Bad Request: the bot's voice language must be ko or en for outbound callsstartVoiceCall: un propósito que el bot no eligió, un idioma del bot distinto del coreano o el inglés
VOICE_INSTRUCTIONS_REQUIRED, VOICE_INSTRUCTIONS_TOO_LONG, VOICE_GREETING_TOO_LONGHacen falta instrucciones · las instrucciones superan 8.192 bytes · el saludo supera 512 bytes
VOICE_INVALID, VOICE_LANGUAGE_INVALID, VOICE_LIMIT_INVALID, VOICE_MODE_INVALIDVoz inexistente · idioma · límite fuera de rango · modo que no es managed/stream
VOICE_CONTENT_REFUSED: <campo>: <código>, VOICE_CONTEXT_REFUSED: <código>Reglas de contenido (Ajustes, el context de startVoiceCall)
VOICE_TOOL_INVALID: <ruta>: <motivo>Definición de herramienta (Definir herramientas)
VOICE_FIELD_NOT_AVAILABLE: …Un campo que no se acepta (model, api_key), campos de stream en un bot gestionado, o campos de las llamadas de bots (outbound·purposes·daily_cap: solo en @BotFather)
VOICE_CALL_NOT_FOUND, VOICE_CALL_NOT_ACTIVE, VOICE_CONTEXT_NOT_SUPPORTED, VOICE_CONTEXT_TOO_LONGMétodos durante la llamada (Durante la llamada)
VOICE_USAGE_PERIOD_INVALIDEl periodo de getVoiceCallUsage
TOOL_QUERY_ID_INVALID, TOOL_QUERY_EXPIRED: …, TOOL_RESULT_…, TOOL_ERROR_TOO_LONGRespuesta de herramienta (Responder)
API_KEY_INVALID, API_KEY_NO_REALTIME_ACCESS, 503 API_KEY_CHECK_UNAVAILABLE, VOICE_PROVIDER_INVALIDClave propia (Clave de modelo propia)
STREAM_URL_INVALID, STREAM_SECRET_REQUIRED, STREAM_SECRET_INVALID, STREAM_PROBE_FAILED: …Modo stream (Configuración y sondeo)
MCP_…, MCP_OPT_IN_REQUIREDServidores MCP (Registrar un servidor). 503 VOICE_NOT_AVAILABLE: mcp: …: vuelve a intentarlo en un rato; 409 MCP_CHECK_IN_PROGRESS: vuelve a intentarlo cuando termine la revisión
429 Too Many Requests: retry after NLímite por hora (ajustes, pruebas de clave, sondeos, MCP), límite de notas o un límite de las llamadas de bots (límites)

Bots conectados por MTProto

  • Un bot conectado solo por MTProto llama a los métodos de voz con bots.sendCustomRequest (el mismo JSON, con el nombre del método). Funcionan: setVoiceCallSettings, getVoiceCallSettings, answerVoiceCallToolQuery, endVoiceCall, sendVoiceCallContext, getVoiceCallUsage, startVoiceCall y los cinco métodos MCP. Todo lo demás: METHOD_INVALID. La respuesta de startVoiceCall es {"voice_call_id": "…"}, y los rechazos llegan como errores RPC (USER_UNAVAILABLE, VOICE_OUTBOUND_OFF: review, FLOOD_WAIT_n, VOICE_UNAVAILABLE).
  • Métodos que no se pueden llamar desde la respuesta de un webhook: setVoiceCallApiKey, startVoiceCall y los métodos MCP de registro y de clave. Puestos en el cuerpo de la respuesta a un POST de webhook, no se ejecutan: así ningún mensaje que mande una persona se convierte directamente en una llamada a través de la respuesta de un webhook. Llámalos con una petición aparte.
  • La clave propia solo por HTTPS: setVoiceCallApiKey y deleteVoiceCallApiKey no se pueden llamar con sendCustomRequest. Para borrar la clave, usa la Bot API HTTP o @BotFather.
  • A un bot sin webhook que no haya llamado a getUpdates en los últimos 15 segundos, voice_call (también el de una llamada que hizo el bot) le llega una vez empujado por updateBotWebhookJSON (independientemente de allowed_updates, sin reenvío). Las consultas de herramienta llegan por updateBotWebhookJSONQuery y se responden con bots.answerWebhookJSONQuery enviando {"result": …} o {"error": "…"}. voice_call_transcript no llega por MTProto (el campo transcript de la actualización de fin de llamada sí llega).
  • En los errores RPC, 429 es FLOOD_WAIT_n, y los demás son el código en mayúsculas tal cual.