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
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ón | Para activarla (todas las condiciones) | pabal.me hoy |
|---|---|---|
| Llamadas a bots de voz | El 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 publicado | Desactivado |
| Llamar desde el Apple Watch | Llamadas 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 bot | Desactivado: 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ón | Desactivado: pendiente de revisión legal |
| Llamadas de bots | Está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és | Desactivado: 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és | Borrador: 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 condiciones | No se pueden aceptar |
| Herramientas | Llamadas a bots de voz + una pasarela que admita herramientas | Desactivado |
| Servidores MCP | Componentes 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 propia | Clave 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ón | El 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 gestionada | Desactivado |
| Modo stream | Clave 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
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_callcuando una llamada empieza y cuando termina, yvoice_call_tool_querycuando 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.
- 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 devuelve403 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 esmessage.from.idengetUpdatesdespués de escribirle al bot). - 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). - 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?» (켤까요?). - 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). - Comprueba que
availableseatrueengetVoiceCallSettingsy 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.
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ámetro | Tipo | Descripción |
|---|---|---|
enabled | Boolean | Activar 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 |
instructions | String | Instrucciones 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 |
greeting | String | Lo primero que dice el bot después del aviso de IA. Hasta 512 bytes. Con "", el modelo saluda brevemente por su cuenta |
voice | String | ID de voz (lista), kore por defecto |
language | String | El 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_duration | Integer | Duració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_calls | Integer | Llamadas que atiende a la vez. Mínimo 1 y máximo limits.max_concurrent_calls (2 por defecto). Valor por defecto del bot: 2 |
tools | Array | Herramientas del bot que el modelo puede llamar durante la llamada (Herramientas). [] las borra todas |
transcript | Boolean | Interruptor de la transcripción (Transcripción) |
mode, stream_url, stream_secret | String | Modo stream (Configurar el modo stream) |
model, api_key | No 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 conVOICE_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 Nyparameters.retry_after. - Si envías un formulario (
-d name=value), pontoolscomo 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
}
}
| Campo | Significado |
|---|---|
enabled | Si el propietario del bot lo activó |
available, unavailable_reason | Si el bot puede recibir una llamada ahora mismo, llame quien llame, y si no, por qué (tabla de abajo) |
rules_accepted, owner_terms_accepted | Si se aceptaron las reglas de los bots de voz y las condiciones actuales para propietarios de bots (versión 4) |
limits | Los valores máximos que se pueden fijar en este servidor y los límites diarios (minutos) |
voices, languages | Voces e idiomas disponibles |
testers_only | true si, antes de la apertura al público, solo pueden llamar los testers que elige el operador |
tools, tool_limits, tool_delivery, tool_delivery_problem | Las 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_ignores | Modo stream: el secreto no aparece nunca, solo una huella de 8 caracteres |
transcript, transcript_available, transcript_unavailable_reason, transcript_delivery, transcript_delivery_problem | El interruptor de la transcripción y si de verdad se entregará |
has_api_key, api_key, own_keys_available | Clave 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_reason | Servidores MCP |
outbound | El estado de las llamadas del bot (Llamadas de bots: ajustes y uso) |
updated_date | Último cambio (segundos Unix) |
unavailable_reason | Significado |
|---|---|
voice_off | La voz está desactivada en este servidor, o la política de privacidad todavía no tiene la sección de llamadas a bots |
kill_switch | El operador detuvo todas las llamadas de voz |
bot_voice_off | El propietario del bot no lo activó |
not_allowed | El operador no autorizó este bot |
awaiting_review | El 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 |
suspended | El operador suspendió la voz de este bot (o de la cuenta de su propietario) |
name_refused, text_refused | El 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_gateway | El servidor no tiene relé de llamadas o no tiene pasarela de voz |
no_gateway_with_tools, no_own_key_gateway | No hay ninguna pasarela que pueda usar las herramientas o la clave propia de este bot |
no_gateway_with_language | No hay conectada ninguna pasarela que atienda llamadas en el idioma de este bot (Voces e idiomas) |
own_key_unavailable, stream_unavailable | La 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.
| id | Carácter | id | Carácter | id | Carácter |
|---|---|---|---|---|---|
zephyr | luminosa | puck | alegre | charon | nítida |
kore | firme | fenrir | entusiasta | leda | juvenil |
orus | firme | aoede | fresca | callirrhoe | tranquila |
autonoe | luminosa | enceladus | aireada | iapetus | clara |
umbriel | tranquila | algieba | suave | despina | suave |
erinome | clara | algenib | áspera | rasalgethi | nítida |
laomedeia | alegre | achernar | delicada | alnilam | firme |
schedar | serena | gacrux | madura | pulcherrima | decidida |
achird | cercana | zubenelgenubi | relajada | vindemiatrix | afectuosa |
sadachbia | animada | sadaltager | erudita | sulafat | cá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.
| id | Idioma | Notas |
|---|---|---|
ko-KR | Coreano | Por defecto |
en-US | Inglés | — |
ja-JP | Japonés | — |
cmn-CN | Chino (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-FR | Francés | — |
es-US | Español | — |
id-ID | Indonesio | — |
vi-VN | Vietnamita | — |
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_languagey 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 conVOICE_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_updateses un único ajuste por bot: se conserva la última lista que se pasó agetUpdatesosetWebhook(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 comofirst_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.
event | Cuándo | Campos adicionales |
|---|---|---|
started | Cuando 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á conectado | max_duration |
ended | Cuando 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 |
missed | Una llamada que terminó antes de establecerse (se colgó mientras sonaba, etc.) | end_reason |
refused | Solo 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 bot | refused_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. Confalse, cuelga de inmediato. Las llamadas en modo stream siempre se cortan de inmediato.- El
voice_call.end_reasonde la llamada terminada esbot. 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. Conrespond=trueel 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_after60).
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) omonth. Conmonthse puede elegir el mes, por ejemplomonth=2026-09(un mes futuro daVOICE_USAGE_PERIOD_INVALID).- Los días y los meses se cuentan según la zona horaria del servidor (
Asia/Seoulpor defecto).remaining_seconds_todayes 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.
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
parametersde 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
}]
}'
| Elemento | Regla |
|---|---|
| Claves | Solo estas cuatro: name, description, parameters (opcional), timeout_ms (opcional) |
| Número y tamaño | 16 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 |
description | Obligatoria, 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 |
parameters | En el nivel superior, {"type": "object"}. Si se omite, «sin argumentos» |
timeout_ms | De 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 únicotype(string·number·integer·boolean·array·object). properties·requiredpara object,items·minItems·maxItemspara array (itemsobligatorio),minimum·maximumpara números,enumsolo 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
}
}
argumentses 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 afrom.id(quien llama, verificado por Pabal).budget_msson los milisegundos desde que el bot la recibe hasta que la respuesta tiene que llegar al servidor. Es eltimeout_msde 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 quetimeout_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).
- Por el webhook, si lo hay.
voice_call_tool_querytiene que estar en elallowed_updatesdel 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. - 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. - Si no, si el bot está conectado por MTProto, por
updateBotWebhookJSONQuery(independientemente deallowed_updates). - 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ámetro | Descripción |
|---|---|
tool_query_id | El voice_call_tool_query.id de la actualización |
result | El 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) |
error | Mensaje de fallo del propio bot, hasta 512 bytes. El modelo se lo transmite a quien llama |
- Exactamente uno de
resultyerror(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) danTOOL_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
errordel 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 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:
- 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.
- 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.
- Guardas el texto recibido solo el tiempo necesario, lo borras si quien llamó lo pide y no lo usas para entrenar modelos.
- 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.
- 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.
- Si quien llama lo pregunta, le dices dónde está tu servidor y qué hace (conviene ponerlo en la presentación del bot).
- 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.»
- 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.
- 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.
- 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.
- 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
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
401honesto 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-mcpcon 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
_metadetools/calllleva el plazo restanteme.pabal/budget_ms(milisegundos) y el tamaño máximo del resultadome.pabal/result_budget_bytes, y puede llevar una aserción firmada por Pabal (me.pabal/assertion, un JWT). El emisor de la aserción, elsub(bot:<id del bot>) y las claves de verificación (JWKS) los dagetVoiceCallMcpBinding. tools/listocupa 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 contools. - La versión 1.0 del perfil solo concede L1: solo se fijan a las llamadas las herramientas con
data: "none"yeffect: "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 porpabal. 2 servidores por bot. El nombre de herramienta que ve el modelo eslabel__nombredeherramienta.auth.type:none,bearer(token),headers(hasta 3 cabeceras),client_credentials(token_url,client_id,client_secret, oprivate_key_jwt: true— antes,createVoiceCallMcpClientKey).authorization_codetodaví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, elsuby 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, endocs/app/voice-bot-mcp-profile.html; si lo necesitas, pídeselo al operador del servidor.
Clave de modelo propia
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:googlepara una clave JSON de cuenta de servicio de Vertex AI (solo se guardan los campos necesarios) yopenaipara una clave de API que empieza porsk-. 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_keysolo 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 (marinpor 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
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 (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
transcriptllega dentro delvoice_callde la llamada terminada (event: "ended"). Es la única versión completa, así que incluyevoice_callenallowed_updates. Si quieres recibir los turnos uno a uno durante la llamada, añade tambiénvoice_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
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).
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íareadyen 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íastopcon 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"
- 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 valelocalhost), y el puerto, 443 o de 1024 a 65535. Si no:400 STREAM_URL_INVALID: …. - 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. - 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. - Sondeo: la pasarela intenta conectarse una vez (
"probe": trueenstart). Si llegaready, 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. - 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_secreten 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ásVOICE_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.
getVoiceCallSettingssolo muestrastream_urly la huella del secreto. En las llamadas en modo stream no funcionasendVoiceCallContext(VOICE_CONTEXT_NOT_SUPPORTED), yendVoiceCallcuelga 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_idybot_idson cadenas decimales.
| Sentido | Mensaje | Cuá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 → bot | Binario | Voz 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 → Pabal | Binario | Audio 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
eventdesconocido 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>")
hostes el host de la URL que diste a Pabal (en minúsculas, sin puerto). No lo calcules a partir de la cabeceraHostde la petición: ese valor lo escribe quien envía, así que no ata la firma a ningún host.path?queryes la ruta y la cadena de consulta de la URL tal cual (/si no hay). En un sondeo,calles el número del sondeo.- El servidor del bot comprueba obligatoriamente:
|ahora − t| ≤ 300 segundos, cadanonceuna 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_idsea igual alcallde 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 questart.bot_idsea su bot. - Valores de prueba: secreto
example-stream-secret-of-the-bot-developer-0001,t=1790000000, nonce00112233445566778899aabbccddeeff, call4611686018427387905, hostbot.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 demarkyclear). 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 respuestacleared,played_mses la suma del audio del bot realmente reproducido en esta llamada hasta ahora,dropped_mslo que se acaba de descartar ymarkslos 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
hangupo 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.reasones 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
startde un sondeo lleva"probe": true, uncallervacío ymax_seconds: 0. Cuando envíasready, 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
languagedel 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
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
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 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ó da400. 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=1a 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ámetro | Significado |
|---|---|
user_id | El id de la persona a la que llamar, que tiene que haber permitido este bot |
purpose | Uno de los propósitos que eligió este bot: reminder · wake_up · delivery · appointment |
context | Lo 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_duration | Lo 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_timeout | Cuánto suena (segundos), de 30 a 45. 40 por defecto |
| Respuesta | Significado | Qué 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és | Corregirlo |
400 VOICE_CONTEXT_REFUSED: <código> | context incumple las reglas de contenido | Corregirlo |
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_UNAVAILABLE | No 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 motivo | Pedir el permiso |
429 retry after N | Un 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_UNAVAILABLE | Lado 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ón | Má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}
}outboundengetVoiceCallSettingssiempre 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_reasonsolo aparece cuandoavailablees false (del lado del bot, los mismos valores que los motivos de403 VOICE_OUTBOUND_OFF; del lado del servidor,voice_off·kill_switch·no_relay·outbound_off: estos cuatro corresponden a503 VOICE_UNAVAILABLE).outboundengetVoiceCallUsagesolo 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·refusednunca 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í quebotyownerderemaining_seconds_todaybajan; mientras suena, ya bajan lo que la llamada reserva, y eso vuelve si nadie contesta.setVoiceCallSettingsno 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
}
}event | Cuándo | Campos extra |
|---|---|---|
started | La 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 |
ended | Terminó 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 pasarela | start_date, duration, max_duration, end_reason |
not_answered | Una 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ón | Ninguno |
failed | Se perdió el enlace con la pasarela que tenía la petición antes de que contestaran | Ninguno |
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 luegonot_answered). Termina una llamada contestada (end_reasonbot). 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 quecontext. 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.
contextva 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):
- el id de Pabal de la persona
- si el permiso está activo (lo muestran las respuestas a sus peticiones de llamada)
- si la persona contestó
- cuándo empezó la llamada y cuánto duró
- quién colgó
- 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ímite | Por defecto | En un servidor de producción |
|---|---|---|
| Un bot a una persona (al día) | 3 llamadas | El operador solo puede bajarlo |
| A una persona (al día, todos los bots) | 6 llamadas | Solo se puede bajar |
| Intervalo entre llamadas a una persona | 10 minutos | Solo se puede subir |
| Peticiones de un bot (al día) | 50 llamadas | El propietario elige 10, 20 o 50 en @BotFather |
| Un propietario de bots (al día, todos sus bots) | 100 llamadas · 30 minutos | Las llamadas solo se pueden bajar |
| Hasta que el bot pueda enviar su siguiente petición | 10 segundos | Solo se puede subir |
| Minutos salientes del servidor | el 30 % de los minutos diarios del servidor | Solo se puede bajar |
| Horas de silencio | 21:00–08:00 | El operador y la persona solo pueden ampliarlas. En la zona horaria del dispositivo |
Franja de despertador (solo wake_up) | como mucho 120 minutos | La persona la elige dentro de sus horas de silencio |
Timbre (ring_timeout) | 30–45 segundos | 40 segundos si la petición no lo indica |
| Pausa: un rechazo, un timbre sin respuesta, colgar en los 10 segundos siguientes a contestar | 24 horas | Solo se puede subir |
| Pausa: el bot retiró la petición en los primeros 15 segundos | 1 hora | Solo 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,
startVoiceCallrecibe429y 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ímite | Por defecto | Quién lo fija |
|---|---|---|
| Duración máxima de una llamada | 300 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 bot | 2 (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 bot | 60 minutos | El operador |
| Al día por propietario de bot (todos sus bots) | 120 minutos | El operador |
| Al día para todo el servidor | 300 minutos | El operador |
| Por persona que llama | 60 minutos al día, 20 llamadas por hora | El operador |
| Llamada más corta que se atiende | 60 segundos: si queda menos tiempo, no se atiende | El operador |
| Cuentas nuevas | Quien 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ía | El operador |
| Cambiar los ajustes | 30 por hora por bot | — |
| Notas durante la llamada | 10 por minuto por llamada | — |
| Herramientas | 16 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 stream | 5 por hora por bot y 10 por propietario | — |
| Registros y revisiones MCP | 10 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ódigo | Significado |
|---|---|
403 VOICE_RULES_NOT_ACCEPTED | Primero hay que aceptar las reglas de los bots de voz en @BotFather |
403 VOICE_NOT_AVAILABLE | Este 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_SUSPENDED | El operador suspendió la voz de este bot. Solo se puede desactivar |
403 VOICE_OWNER_TERMS_NOT_ACCEPTED | Primero hay que aceptar las condiciones para propietarios de bots (versión 4) |
403 VOICE_OWN_KEY_NOT_AVAILABLE | Este servidor no acepta claves propias |
403 VOICE_STREAM_NOT_AVAILABLE | El 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_UNAVAILABLE | No se puede llamar a esta persona: siempre el mismo cuerpo sea cual sea el motivo, incluida la falta de permiso |
503 VOICE_UNAVAILABLE | El 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 calls | startVoiceCall: 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_LONG | Hacen falta instrucciones · las instrucciones superan 8.192 bytes · el saludo supera 512 bytes |
VOICE_INVALID, VOICE_LANGUAGE_INVALID, VOICE_LIMIT_INVALID, VOICE_MODE_INVALID | Voz 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_LONG | Métodos durante la llamada (Durante la llamada) |
VOICE_USAGE_PERIOD_INVALID | El periodo de getVoiceCallUsage |
TOOL_QUERY_ID_INVALID, TOOL_QUERY_EXPIRED: …, TOOL_RESULT_…, TOOL_ERROR_TOO_LONG | Respuesta de herramienta (Responder) |
API_KEY_INVALID, API_KEY_NO_REALTIME_ACCESS, 503 API_KEY_CHECK_UNAVAILABLE, VOICE_PROVIDER_INVALID | Clave 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_REQUIRED | Servidores 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 N | Lí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,startVoiceCally los cinco métodos MCP. Todo lo demás:METHOD_INVALID. La respuesta destartVoiceCalles{"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,startVoiceCally 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:
setVoiceCallApiKeyydeleteVoiceCallApiKeyno se pueden llamar consendCustomRequest. Para borrar la clave, usa la Bot API HTTP o @BotFather. - A un bot sin webhook que no haya llamado a
getUpdatesen los últimos 15 segundos,voice_call(también el de una llamada que hizo el bot) le llega una vez empujado porupdateBotWebhookJSON(independientemente deallowed_updates, sin reenvío). Las consultas de herramienta llegan porupdateBotWebhookJSONQueryy se responden conbots.answerWebhookJSONQueryenviando{"result": …}o{"error": "…"}.voice_call_transcriptno llega por MTProto (el campotranscriptde la actualización de fin de llamada sí llega). - En los errores RPC,
429esFLOOD_WAIT_n, y los demás son el código en mayúsculas tal cual.