Números comerciales y menús telefónicos para Estados Unidos y Canadá. ENFR(510) 288-6288[email protected]Iniciar sesión

Inicio › Twilio › Crear un IVR

Twilio

Cree un menú telefónico comercial en Twilio, de principio a fin.

Un menú telefónico de Twilio requiere unas pocas docenas de líneas de TwiML para una demostración y unos cientos para funcionar bien. La diferencia no está en el menú, sino en todo lo que lo rodea: la persona que no presiona nada, el celular cuyo propio buzón de voz contesta primero, el día festivo que nadie puso en el código. Esta guía construye todo, con cada atributo verificado contra la documentación de Twilio tal como se lee hoy.

Revisado el 26 de octubre de 2026 · 31 fuentes citadas · 29 min de lectura

Building a phone menu with code

Lo que tendrá al final

Un número de Twilio con un menú de tres opciones: ventas, soporte y horario de atención, más el 0 para dejar un mensaje. Se comporta como una pequeña empresa necesita que se comporte un menú:

  • El horario y los días festivos se evalúan en la zona horaria de la propia empresa, de modo que el cambio de horario de verano y un servidor en otra región no desplazan su hora de apertura.
  • Inglés y francés. El saludo ofrece el francés con el 9; el menú en francés usa una voz de francés canadiense y reconoce tanto el francés hablado como las teclas.
  • Ventas hace sonar dos teléfonos a la vez; soporte hace sonar un teléfono y luego el siguiente. Ambos pasan al buzón de voz si nadie contesta.
  • Filtrado de llamadas en el teléfono que contesta, para que el buzón de voz del celular de un integrante del equipo no “conteste” en silencio la llamada de la empresa.
  • Sin respuesta y teclas incorrectas reciben una breve disculpa y el menú de nuevo, dos veces; después, el buzón de voz. Nunca un ciclo interminable ni una línea muerta.
  • El mensaje de voz llega por correo electrónico con el audio adjunto. Twilio lo graba; su código envía el correo. Ninguno de los verbos de TwiML ni el recurso Recording que se documentan abajo envía correo electrónico.

El código es Node.js con Express y la biblioteca auxiliar de Twilio. Se ejecutó localmente para esta guía con twilio 6.2.0 y Express 5.3.0, y los ejemplos de TwiML de abajo son lo que realmente imprimió. El package.json de la propia biblioteca declara la versión 6.2.0 y Node 20 o posterior.31 Si prefiere diseñar el flujo de la llamada antes de escribir código, nuestras plantillas de flujo de llamadas son un buen primer paso, y qué es una operadora automática explica el vocabulario.

Todo en esta guía es documentación pública de Twilio, consultada el 26 de octubre de 2026, más nuestras propias pruebas y opiniones, que se identifican como tales. Es una guía editorial para desarrolladores; no dice nada sobre cómo está construido ningún servicio alojado en particular, incluido el nuestro.

Cómo llega una llamada entrante a su código

Cada número de Twilio tiene una URL de voz: “The URL we call when the phone number receives a call.” (la URL a la que Twilio llama cuando el número recibe una llamada).2 La configura en la Consola, mediante el recurso REST IncomingPhoneNumber o con la CLI de Twilio (más abajo). También existe una voice_fallback_url: “The URL that we call when an error occurs retrieving or executing the TwiML requested by url” (la URL a la que Twilio llama cuando ocurre un error al obtener o ejecutar el TwiML solicitado).2 Apúntela a algo que no sea su servidor principal, incluso un archivo TwiML estático que diga que hay un problema y tome un mensaje; es el seguro contra caídas más barato de toda la solución.

Cuando llega una llamada, Twilio hace una solicitud HTTP a esa URL. Con POST (la opción habitual y el método que usa esta guía) los parámetros llegan en un cuerpo codificado como formulario; con GET llegan como parámetros de consulta.1 Los que le importan a un menú telefónico:

ParámetroLo que documenta Twilio
CallSid“A unique identifier for this call, generated by Twilio.” (un identificador único de esta llamada, generado por Twilio). Úselo como clave de todo lo que registre.1
From, ToQuien llama y quien recibe, en formato E.164 para números de teléfono. El identificador de llamada puede estar oculto, así que From puede tener un valor como “anonymous”.1
CallStatusUno de queued, initiated, ringing, in-progress, completed, busy, failed, no-answer, canceled.1
Directioninbound para la llamada que entra; outbound-dial para los tramos que crea su <Dial>.1
ForwardedFromSolo se define cuando la llamada se desvió a Twilio y el operador que la desvió pasó la información, algo que “not all carriers” (no todos los operadores) hacen.1
CallerNameSolo cuando la búsqueda del nombre del llamante está activada en el número, que la referencia de la API fija en “$0.01 per look up” (US$0.01 por consulta).12
FromCity, FromState, FromZip, FromCountry (y To…)“Twilio also attempts to look up geographic data based on the To and From phone numbers.” (Twilio también intenta buscar datos geográficos a partir de los números To y From). Es un esfuerzo razonable; nunca enrute solo con base en ellos.1

Desplace la tabla hacia los lados para ver todas las columnas.

Su respuesta es TwiML: un documento XML cuyos verbos Twilio ejecuta “in order from top to bottom” (en orden, de arriba hacia abajo).1 Dos reglas que hacen tropezar a todo el que construye por primera vez: “TwiML elements (verbs and nouns) have case-sensitive names” (los nombres de los elementos distinguen mayúsculas de minúsculas), y los nombres de los atributos son “case sensitive and camelCased” (distinguen mayúsculas y usan camelCase). El ejemplo de Twilio es que escribir <say> en lugar de <Say> “will result in an error.” (producirá un error).1

La regla de los 15 segundos

Twilio “imposes a hard upper timeout of 15 seconds on all call-related HTTP requests” (impone un tiempo límite máximo estricto de 15 segundos a todas las solicitudes HTTP relacionadas con llamadas), y ese tope anula cualquier tiempo límite que usted configure.3 Un webhook de menú que espera una consulta lenta a un CRM o un contenedor que arranca en frío deja a la persona en silencio y luego falla. Mantenga la ruta de la llamada en lógica pura y configuración; haga el trabajo lento (correo, escrituras en el CRM, transcripción) en las devoluciones de llamada de estado, después de haber respondido.

Los verbos de TwiML que usa un menú telefónico

Un menú telefónico necesita ocho piezas de TwiML. Esta es la referencia que nos habría gustado tener: cada atributo que usa esta guía, su valor predeterminado y la fuente. Los valores predeterminados importan porque la biblioteca auxiliar omite cualquier atributo que usted no defina, así que lo que Twilio use por defecto es lo que oirán sus llamantes.

Verbo o sustantivoAtributoValor predeterminado (según la documentación)Notas
<Say>voiceman en la tabla; el texto agrega que el valor predeterminado “depends on your account-level Text-to-Speech settings” (depende de la configuración de texto a voz de su cuenta)Las voces básicas man/woman son gratuitas; las voces de Amazon Polly y Google se nombran Proveedor.Voz.4
languageen-USLa voz debe admitir el idioma, o el <Say> puede fallar.4
loop10 repite hasta que se cuelgue o hasta 1,000 repeticiones.4
<Play>loop1Se aceptan MP3, WAV, AIFF, GSM y μ-law; Twilio transcodifica y guarda en caché.6
<Gather>inputdtmfO speech, o dtmf speech.7
numDigitssin límiteDefínalo en 1 para un menú de una sola tecla, de modo que la persona no tenga que presionar #.7
timeout5 segundosComienza cuando termina de reproducirse el mensaje anidado.7
finishOnKey#7
action / methodURL del documento actual / POST7
actionOnEmptyResultfalseCon true, Twilio llama a action aunque no se haya ingresado nada.7
languageen-USIdioma del reconocimiento de voz; el conjunto admitido depende de speechModel.7
<Dial>timeout30 segundosMínimo 5, máximo 600.8
actionningunoSin él, Twilio pasa al siguiente verbo y cuelga si no hay ninguno.8
callerIdel identificador de llamada de quien llama8
answerOnBridgefalsetrue hace que quien llama siga oyendo el tono de llamada hasta que conteste el destino.8
timeLimit14,400 segundos (4 horas)8
<Number>urlningunoTwiML que se ejecuta en el teléfono que contesta antes de conectar a las partes.9
<Record>timeout5 segundos de silencio0 lo desactiva.10
maxLength3600 (1 hora)10
finishOnKey1234567890*#Cualquier tecla termina la grabación, a menos que la restrinja.10
playBeep / trimtrue / trim-silence10
transcribefalseDe pago, solo inglés estadounidense, de 2 a 120 segundos.10
recordingStatusCallbackEventcompletedTambién in-progress, absent.10
<Redirect>methodPOST“All verbs after <Redirect> are unreachable and ignored.” (todos los verbos posteriores a <Redirect> son inalcanzables y se ignoran). Se permiten URL relativas.11
<Hangup>ninguno“The <Hangup> verb ends a call.” (el verbo <Hangup> termina una llamada).12

Desplace la tabla hacia los lados para ver todas las columnas.

Dos detalles de esa tabla cambian la forma de escribir un menú. Primero, el finishOnKey predeterminado de <Record> es cualquier tecla, así que quien presiona una tecla mientras deja un mensaje termina la grabación; nosotros lo restringimos a #. Segundo, <Hangup> contesta la llamada antes de terminarla: Twilio señala que ponerlo primero no evita que la llamada se conteste y se cobre, y que solo <Reject> lo evita.12 Si bloquea un número, recházelo en lugar de colgarle.

Gather: el menú en sí y las dos maneras en que falla

<Gather> reproduce el <Say> o <Play> anidado (acepta esos dos y <Pause>)7 y escucha mientras reproduce: la documentación describe el tiempo límite como algo que comienza “either after the audio completes or when the caller presses their first key.” (ya sea cuando termina el audio o cuando quien llama presiona su primera tecla).7 Así, quien llama con frecuencia y conoce el menú puede presionar el 2 durante el saludo y saltarse el resto. Es la mayor mejora de usabilidad de un menú telefónico, y se obtiene simplemente anidando el mensaje dentro de <Gather> en lugar de poner un <Say> antes.

Las dos rutas de fallo son lo que separa una demostración de un menú que puede poner en una línea comercial.

La persona no presiona nada. “By default, if <Gather> times out while waiting for input, it continues to the next TwiML instruction.” (de forma predeterminada, si <Gather> agota el tiempo esperando una entrada, continúa con la siguiente instrucción de TwiML).7 En un documento que termina con el <Gather>, la siguiente instrucción es nada, y la llamada termina. Defina actionOnEmptyResult="true" y Twilio solicitará su URL action “regardless of whether input is received” (sin importar si se recibió una entrada),7 de modo que todos los resultados llegan al mismo controlador y la decisión queda en un solo lugar. Ese es el patrón que usa esta guía.

La persona presiona una tecla que usted no usa. Twilio se la envía como Digits, igual que cualquier otra tecla; en TwiML no existe la noción de tecla válida. Su controlador debe tratar todo lo que esté fuera del menú como no válido, decirlo y repetir el menú. Cuente los intentos en la URL (?tries=1) y, tras el tercer fallo, enrute a una persona o al buzón de voz. Nuestra opinión: tres reproducciones es lo máximo que debería oír una persona; quien no ha elegido después de tres está perdido, en una línea de marcación por pulsos o es un robot, y a ninguno lo ayuda una cuarta.

Voz además de teclas. input="dtmf speech" acepta cualquiera de las dos, y “Twilio gives precedence to the first input it detects.” (Twilio da prioridad a la primera entrada que detecta).7 La voz reconocida llega como SpeechResult con una puntuación Confidence entre 0.0 y 1.0, que según Twilio su código “shouldn’t expect” (no debe esperar) que esté presente.7 El atributo hints recibe las palabras que espera, que en un menú son pocas y conocidas. El modelo predeterminado phone_call incluye fr-CA entre sus idiomas, junto con en-US.7 Una trampa: speechTimeout="auto" termina el reconocimiento en la primera pausa, pero la documentación dice que si define speechModel debe usar un entero positivo y no auto.7 El ejemplo de abajo deja speechModel en su valor predeterminado.

<Response>
  <Gather input="dtmf speech" numDigits="1" timeout="5" speechTimeout="auto"
          language="fr-CA" hints="ventes, soutien, heures, message"
          action="/menu/choice?lang=fr&amp;tries=0" actionOnEmptyResult="true">
    <Say voice="Polly.Gabrielle-Neural" language="fr-CA">Pour les ventes, faites le 1 ou dites ventes. …</Say>
  </Gather>
</Response>

Observe el &amp;: un ampersand dentro de un atributo debe escaparse en XML. La biblioteca auxiliar lo hace por usted, una razón más para no construir TwiML concatenando cadenas.

Voces y un menú bilingüe en inglés y francés

Las voces de <Say> vienen en niveles. Las voces básicas (man, woman) son gratuitas; Amazon Polly y Google ofrecen voces Standard, Neural y Generative, nombradas con el proveedor como prefijo, por ejemplo Polly.Joanna-Generative.4 Las voces Generative están en Public Beta, lo que según Twilio significa que no están cubiertas por su SLA.5 Si define voice y language a la vez, el par debe aparecer en la tabla de Twilio: “Combinations of voice and language not listed as available may result in an error and <Say> instruction failure.” (las combinaciones de voz e idioma que no figuran como disponibles pueden producir un error y el fallo de la instrucción <Say>).4

Para el francés canadiense, la tabla de voces de Twilio lista, bajo fr-CA, las voces de Polly Chantal (Standard), Gabrielle-Neural y Liam-Neural, y voces Standard, WaveNet, Neural2 y Chirp3-HD de Google.5 Nosotros usamos Polly.Gabrielle-Neural con language="fr-CA" y, para inglés, Polly.Joanna-Neural con en-US. No encontramos una fila en-CA en la tabla de voces, así que un menú en inglés para llamantes canadienses usa una voz estadounidense (o británica). Dos límites más de la misma página: las etiquetas SSML que Twilio lista, <break> y <phoneme>, se pueden usar “only for the en-US language” (solo para el idioma en-US), y las voces básicas “can process no more than 4,000 characters.” (no pueden procesar más de 4,000 caracteres).5

Cómo ofrecer dos idiomas. Hay tres diseños comunes: elegir el idioma antes que nada (“for English press 1, pour le français faites le 2”), un saludo bilingüe que continúa en el idioma mayoritario con una tecla para el otro, o un número distinto por idioma. Nuestra opinión: el segundo es la mejor opción predeterminada para una empresa cuyos llamantes hablan en su mayoría un solo idioma, porque quien llama en el idioma mayoritario no oye ningún paso adicional. El ejemplo saluda en inglés, dice “Pour le service en français, faites le 9” con una voz francesa y espera cuatro segundos. Sin tecla, o con cualquier tecla distinta del 9, continúa en inglés. Elija el valor predeterminado contrario si la mayoría de sus llamantes habla francés. El idioma viaja entonces en cada URL (?lang=fr), de modo que cada paso posterior, incluido el buzón de voz, se mantiene en el idioma de quien llama.

Mensajes grabados en lugar de texto a voz. <Play> recibe la URL de un archivo de audio. Twilio lo guarda en caché “when HTTP headers allow it (via ETag and Last-Modified headers)” (cuando los encabezados HTTP lo permiten, mediante ETag y Last-Modified), y advierte que los archivos con pérdida y alta tasa de bits, como un MP3 de 128 kbps, “will take longer to transcode and potentially sound worse” (tardarán más en transcodificarse y posiblemente suenen peor).6 Un saludo grabado profesionalmente en cada idioma suena mejor que cualquier voz sintética, pero cada cambio de redacción se convierte en una sesión de estudio. Nuestra opinión: empiece con texto a voz neural, mantenga las palabras en la configuración y grabe la redacción final cuando el menú haya dejado de cambiar.

Hacer sonar a las personas: simultáneo, secuencial y la trampa del buzón de voz

Timbre simultáneo. Ponga varios sustantivos <Number> dentro de un mismo <Dial>: “You can use up to ten <Number> nouns within a <Dial> verb to simultaneously call all of them at once. The first call to pick up is connected to the current call and the rest are hung up.” (puede usar hasta diez sustantivos <Number> dentro de un verbo <Dial> para llamar a todos a la vez; la primera llamada que contesta se conecta con la llamada actual y las demás se cuelgan).9

Timbre secuencial (un teléfono y luego el siguiente) se hace con la devolución de llamada action. Cuando termina el <Dial>, Twilio solicita la URL de la acción con el resultado y continúa la llamada con el TwiML que usted devuelva.8 Su controlador mira el resultado y termina la llamada o marca el siguiente número. Los parámetros que recibe:

  • DialCallStatus: completed, answered (conferencias), busy, no-answer, failed o canceled.8
  • DialBridged: “Boolean indicator of whether or not the dialing call has been connected to the dialed destination.” (indicador booleano de si la llamada que marca se conectó o no con el destino marcado).8
  • DialCallSid y DialCallDuration.8

La página de <Dial> de Twilio también lista un atributo sequential: definirlo en true “allows you to call up to ten agents one after the other.” (le permite llamar hasta a diez agentes uno tras otro).8 A hoy, la página no da un valor predeterminado ni un ejemplo, y no dice cómo se aplica timeout a cada número. Nosotros usamos el patrón de la devolución de llamada porque cada paso está documentado y le permite decidir entre intentos (saltar a una persona que hoy no trabaja, reproducir un mensaje, detenerse en el buzón de voz). Si prueba sequential, verifique qué hace cuando un teléfono está ocupado o fuera de alcance antes de depender de él.

Tiempo límite e identificador de llamada. timeout es cuánto tiempo hace sonar Twilio antes de rendirse: 30 segundos de forma predeterminada, mínimo 5, máximo 600.8 De forma predeterminada, el teléfono al que se llama “sees the inbound caller’s number as the caller ID” (ve el número de quien llama como identificador de llamada),8 que suele ser lo que un equipo quiere: ver quién llama. Defina callerId con su número de Twilio si prefiere que sepan que es una llamada de la empresa. answerOnBridge="true" mantiene el tono de llamada para quien llama hasta que alguien realmente conteste.8

La trampa que todo tutorial omite

Un celular que no contesta envía la llamada a su propio buzón de voz, y para Twilio eso es una llamada contestada. Con un <Dial> simple, quien llama termina en el buzón personal de un empleado, su respaldo secuencial nunca se ejecuta y, con timbre simultáneo, los demás teléfonos se cuelgan porque el buzón de voz “contestó” primero.9

La solución es el filtrado. El atributo url del sustantivo <Number> devuelve TwiML “to be run on the called party’s end, after they answer, but before the parties are connected” (que se ejecuta del lado de la persona llamada, después de que contesta pero antes de conectar a las partes), y Twilio sugiere exactamente este uso: “You could also provide a chance to decline the phone call using <Gather> and <Hangup>.” (también podría dar la oportunidad de rechazar la llamada con <Gather> y <Hangup>).9 El teléfono que contesta oye “Business line call. Press 1 to accept.” Una persona presiona 1; un saludo de buzón de voz no lo hace, así que el TwiML de filtrado cuelga ese tramo antes de que se conecte con quien llama, y su controlador action tiene la oportunidad de probar con el siguiente teléfono o tomar un mensaje.

Decidimos el éxito con DialBridged, no con DialCallStatus. Twilio no documenta qué estado recibe un tramo cuando la persona llamada contesta y luego no pasa el filtrado, y “conectada o no” es la pregunta que realmente nos importa. Registre ambos durante las pruebas para ver lo que producen su operador y sus teléfonos. El filtrado ayuda menos con el timbre simultáneo. La regla de Twilio es que la primera llamada que contesta se conecta y las demás se cuelgan;9 la documentación no dice si eso ocurre cuando un teléfono contesta o solo cuando termina su TwiML de filtrado. Si es lo primero, un buzón de voz de celular que contesta primero le cuesta los demás teléfonos, y quien llama va a su buzón de voz aunque un colega pudiera haber contestado. Pruébelo con sus propios teléfonos. Nuestra opinión: use filtrado en los grupos secuenciales, y en los simultáneos solo donde los teléfonos tengan de forma confiable el buzón de voz personal desactivado o configurado para contestar después de más tiempo que su timeout.

Horario comercial y días festivos, en la zona horaria correcta

El horario de atención pertenece a la empresa, no al servidor. Un servidor en UTC, un entorno de Functions en otro lugar o una laptop en Vancouver que prueba un menú de Montréal darán cada uno una respuesta distinta a “¿son las 9 a. m.?”. Use un nombre de zona horaria IANA como America/Toronto con Intl.DateTimeFormat de JavaScript, que acepta “any IANA time zone name” (cualquier nombre de zona horaria IANA) y, si no, recurre a la zona del propio entorno de ejecución.27 Nunca guarde un desfase fijo como -05:00: es incorrecto la mitad del año.

El ejemplo convierte “ahora” al día de la semana, la fecha y la hora de 24 horas locales de la empresa (hourCycle: 'h23'27), y luego revisa una lista de días festivos y una tabla por día con horas de apertura y cierre. Lo probamos a ambos lados del fin del horario de verano el 1 de noviembre de 2026: las 08:31 locales estaban abiertas tanto el 26 de octubre (12:30 UTC) como el 2 de noviembre (13:31 UTC), y las 17:01 locales estaban cerradas.

Los días festivos son una lista que usted mantiene. No existe una lista universal que importar. En Canadá, la propia lista de la agencia federal de ingresos señala que “several provinces and territories have their own unique holidays” (varias provincias y territorios tienen sus propios feriados): en 2026, el Día de San Juan Bautista, el 24 de junio, es solo de Quebec y el Día Cívico, el 3 de agosto, excluye a Quebec.28 En Estados Unidos, la lista federal la fija 5 U.S.C. 6103 para los empleados federales, y OPM señala que las empresas privadas pueden usar otros nombres; para 2026 también traslada el Día de la Independencia al viernes 3 de julio porque el 4 de julio es sábado.29 Qué días cierra una empresa privada es decisión de la empresa. Nuestra opinión: mantenga la lista en la configuración junto al horario, agregue los días del próximo año cada otoño y añada un interruptor manual de “cerrado ahora” para días de nieve y emergencias, porque son los días en que quienes llaman más necesitan el mensaje correcto.

El buzón de voz y cómo hacerlo llegar a una bandeja de entrada

<Record> graba a quien llama y, cuando termina la grabación, solicita su URL action con RecordingUrl, RecordingDuration y Digits, la tecla que terminó la grabación o hangup.10 Defina una action: “If no action is provided, <Record> will request the current document’s URL” (si no se indica action, <Record> solicitará la URL del documento actual), algo que Twilio advierte que “can lead to unwanted looping behavior” (puede causar un comportamiento de ciclo no deseado).10 En un controlador como el nuestro, eso significaría que el mensaje del buzón y el tono volverían a empezar después de cada mensaje.

El detalle que rompe la mayoría del código de buzón de voz a correo: “The recording file may not yet be accessible when the action callback is sent.” (es posible que el archivo de la grabación aún no sea accesible cuando se envía la devolución de llamada action). El consejo de Twilio es usar recordingStatusCallback “for reliable notification on when the recording is available” (para una notificación confiable de cuándo está disponible la grabación).10 Por eso el controlador de action solo agradece a quien llamó y cuelga; el correo se envía desde la devolución de llamada de estado, que recibe RecordingSid, RecordingUrl, RecordingStatus (completed o failed) y RecordingDuration, entre otros.1013

Por qué adjuntamos el audio en lugar de enlazarlo. “Twilio enforces HTTP Basic Authentication for all media URLs” (Twilio exige autenticación HTTP Basic en todas las URL de medios), con el SID y el secreto de una clave de API; agregue .mp3 o .wav a la URL de la grabación para elegir el formato (MP3 a 32 kbps, WAV a 128 kbps).13 Por lo tanto, un enlace simple en un correo no se reproducirá para quien lo lee. El ejemplo descarga el MP3 con una clave de API y lo adjunta con Nodemailer, cuyo content de adjunto puede ser “a string, a Buffer, or a Node.js readable stream” (una cadena, un Buffer o un flujo legible de Node.js).30 Un mensaje de voz de dos minutos a 32 kbps pesa unos 480 KB. Envíelo con el servicio de correo que ya usa; nada de este paso involucra a Twilio.

Transcripción. transcribe="true" es la opción sencilla y es limitada: se restringe a grabaciones “with a duration greater than two seconds and less than 120 seconds” (con una duración de más de dos segundos y menos de 120), “only supported in American English” (solo admite inglés estadounidense) y es “a paid feature” (una función de pago).10 El texto llega después a transcribeCallback como TranscriptionText con un TranscriptionStatus de completed o failed.10 El ejemplo la activa solo para inglés y envía la transcripción en un segundo correo. Para mensajes de voz en francés necesita otro servicio de voz a texto, aplicado al MP3 que ya descargó; eso queda fuera de esta guía.

Conservación. Las grabaciones almacenadas se facturan por minuto al mes (vea los costos más abajo), y la referencia de grabaciones indica que los metadatos se conservan 40 días después de eliminarlas.13 Si el correo es el registro del mensaje, elimine la copia de Twilio una vez enviado el correo. Un mensaje de voz es información personal, y nuestra lista de verificación de grabación de llamadas (en inglés) cubre las obligaciones que lo rodean.

Twilio Functions, su propio servidor o Studio

El mismo flujo de llamada puede ejecutarse en tres lugares. Esto es lo que significa cada uno en la práctica.

Twilio FunctionsSu propio servidorTwilio Studio
Qué esControladores de Node.js alojados por Twilio, exports.handler = (context, event, callback)17Cualquier servicio HTTPS que devuelva TwiML (esta guía)“a low-code/no-code visual builder” (un constructor visual de poco código o sin código)21 con widgets de voz como Gather Input On Call, Connect Call To y Record Voicemail22
Límites estrictosLímite de ejecución de 10 segundos; 30 invocaciones simultáneas por cuenta18Solo el límite de 15 segundos de Twilio para webhooks3No se indican en la página de descripción general21
Seguridad del webhookLa visibilidad “Protected” exige un X-Twilio-Signature válido19Usted valida la firma (más abajo)Se maneja dentro de Twilio
Precio (página de Twilio, “current as of August 2026”)Las primeras 10,000 invocaciones al mes son gratis; después, US$0.0001 cada una20Su alojamiento1,000 ejecuciones de flujo al mes gratis; después, US$0.0025 cada una20
Control de versionesArchivos de códigoArchivos de códigoEl flujo es JSON; la API REST puede crear y actualizar un flujo a partir de una definición JSON y lleva un número de revisión23

Desplace la tabla hacia los lados para ver todas las columnas.

Las tres se facturan además de los minutos de llamada; de Studio, Twilio dice que “Additional fees for the underlying Twilio services are not included in Studio pricing.” (los cargos adicionales de los servicios subyacentes de Twilio no están incluidos en el precio de Studio).21

Dos detalles de Functions importan para esta solución. Después de callback(), “the runtime terminates execution, including any asynchronous operations that are still running” (el entorno termina la ejecución, incluidas las operaciones asíncronas que sigan en curso),17 así que el patrón de responder primero y enviar el correo después que se usa abajo no funciona ahí: el correo debe enviarse antes de llamar a callback(), dentro de los 10 segundos. Y Functions solo admite CommonJS, lo que descarta los paquetes npm que son solo ESM.18

Nuestra opinión. Studio es la opción correcta cuando quien administra el menú no es desarrollador y la lógica es un puñado de ramas; es la opción equivocada cuando quiere pruebas, revisión de código o lógica de días festivos, porque un flujo visual es difícil de comparar entre versiones. Functions es excelente para un menú como este: sin servidor y con verificación de firma integrada. Cada solicitud de webhook es una invocación, y este flujo hace de ocho a diez por llamada contestada (menú, elección, resultado del marcado, filtrado, devoluciones de llamada de grabación), de modo que las 10,000 invocaciones gratuitas cubren aproximadamente mil llamadas al mes.20 Ejecute su propio servidor cuando el menú deba leer sus propios sistemas (un CRM, un rol de guardias), cuando necesite trabajo que dure más de 10 segundos o cuando quiera la lógica de la llamada junto al resto de su código. El ejemplo está escrito para Express, pero cada controlador es una función pura de solicitud a TwiML, así que pasarlo a Functions consiste sobre todo en reemplazar req.body por event y res.send por callback.

Pruebas locales: ngrok, la CLI de Twilio y las firmas

Twilio tiene que llegar a su laptop por la internet pública, así que necesita un túnel. La documentación de la CLI de Twilio es directa al respecto: “The Twilio CLI does not accept localhost or 127.0.0.1 URLs directly. You must use a tunneling service like ngrok” (la CLI de Twilio no acepta directamente URL localhost ni 127.0.0.1; debe usar un servicio de túnel como ngrok).24

# terminal 1: the app (NODE_ENV=test switches off signature checks)
NODE_ENV=test node server.js

# terminal 2: a public URL for port 3000
ngrok http 3000

# terminal 3: point your number's voice URL at the tunnel
twilio phone-numbers:update PNXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX   --voice-url https://<your-tunnel>.ngrok.app/voice

La guía de Twilio sugiere observar cada solicitud entrante en los registros de su servidor y en la interfaz web de ngrok en http://127.0.0.1:4040,24 que es la forma más rápida de ver qué envió Twilio y qué respondió usted. Twilio también advierte que un túnel “temporarily exposes your computer to the internet” (expone temporalmente su computadora a internet); deténgalo cuando termine.24

Antes de llamar al número, pruebe el TwiML con HTTP simple. Cada controlador es un POST con campos de formulario, así que puede manejar todo el menú desde una terminal:

curl -s -X POST 'localhost:3000/menu/choice?lang=en&tries=0' -d Digits=7
# <Response><Say …>Sorry, that is not one of the options.</Say>
#   <Redirect>/menu?lang=en&amp;tries=1</Redirect></Response>

Validación de firmas. Twilio firma cada solicitud: calcula un hash de la URL completa más los parámetros ordenados alfabéticamente con HMAC-SHA1, con su token de autenticación como clave, y envía el resultado en X-Twilio-Signature. Recomienda usar el validador del SDK y dice “Don’t implement your own signature validation.” (no implemente su propia validación de firmas).14 En Express eso es twilio.webhook(), que devuelve 403 a las solicitudes que fallan y lee el token de TWILIO_AUTH_TOKEN.15 El tutorial de Twilio advierte que la validación falla detrás de ngrok o de un proxy que termina TLS porque “the request URL that your Express application sees does not match the URL Twilio used” (la URL de la solicitud que ve su aplicación Express no coincide con la URL que usó Twilio).15 La biblioteca reconstruye la URL a partir de request.protocol, el encabezado Host y originalUrl, y acepta protocol, host o una url completa como valores de reemplazo.16 En nuestra prueba bastó con app.set('trust proxy', true): con un encabezado X-Forwarded-Proto https y el nombre de host del túnel, una solicitud correctamente firmada pasó y una firma incorrecta recibió 403. Confíe en los encabezados de proxy solo cuando delante de la aplicación haya un proxy que usted controle.

Por último, pruebe las rutas que no ocurren en una llamada sin contratiempos: deje que el menú agote el tiempo tres veces, presione 7, deje que suenen sin respuesta los dos teléfonos de soporte, conteste uno y no presione 1, cuelgue a la mitad de un mensaje de voz y llame en una fecha de su lista de días festivos (agregue temporalmente la de hoy). Cada una debería terminar en un lugar deliberado.

El ejemplo completo, con anotaciones

Un archivo, de unas 300 líneas. Instale con npm install twilio express nodemailer. Variables de entorno: TWILIO_AUTH_TOKEN (validación de firmas), TWILIO_ACCOUNT_SID, TWILIO_API_KEY y TWILIO_API_SECRET (para descargar grabaciones y consultar a quien llama), SMTP_URL, VOICEMAIL_FROM y VOICEMAIL_TO (su servicio de correo) y, opcionalmente, TWILIO_NUMBER. Los números de teléfono son marcadores de posición. Cómo encajan las piezas:

  1. Configuración (sección 1): zona horaria, horario, días festivos, los dos grupos, el tiempo de timbre y cada mensaje en ambos idiomas. Una empresa cambia esta sección y nada más.
  2. Horario (sección 2): isOpen() evalúa el momento actual en la zona horaria de la empresa.
  3. Entrada (sección 4): cerrado significa un mensaje de cierre bilingüe y una grabación; abierto significa el saludo bilingüe con el 9 para francés, y luego /menu.
  4. Menú y elección (sección 5): un solo <Gather> con actionOnEmptyResult, de modo que la falta de entrada, las teclas incorrectas y las teclas válidas llegan todas a /menu/choice, que cuenta los intentos.
  5. Ventas hace sonar ambos números a la vez con filtrado; soporte (sección 6) recorre su lista un número a la vez mediante /support/next. Ambos toman DialBridged como prueba de éxito.
  6. El filtrado (sección 7) se ejecuta en el teléfono que contesta.
  7. Buzón de voz (secciones 8 y 9): grabar, agradecer, colgar; el correo sale desde la devolución de llamada de estado de la grabación, después de que Twilio ya recibió su respuesta.
// server.js: a three-option business phone menu on Twilio Programmable Voice.
// Node 20+, Express 5, twilio 6. Run: node server.js
'use strict';
const express = require('express');
const twilio = require('twilio');
const nodemailer = require('nodemailer');

const { VoiceResponse } = twilio.twiml;

// ---- 1. Configuration: everything a business would change lives here ----
const CONFIG = {
  timeZone: 'America/Toronto',              // IANA name, never a fixed UTC offset
  // Opening hours per weekday, 24-hour local time. Missing day = closed.
  hours: {
    Mon: ['08:30', '17:00'], Tue: ['08:30', '17:00'], Wed: ['08:30', '17:00'],
    Thu: ['08:30', '17:00'], Fri: ['08:30', '16:00'],
  },
  // Days you are closed, in local time. You maintain this list yourself.
  holidays: new Set(['2026-11-11', '2026-12-25', '2026-12-26', '2027-01-01']),
  sales:   ['+15145550101', '+15145550102'],  // ring together
  support: ['+15145550111', '+15145550112'],  // ring one after the other
  ringSeconds: 18,                          // per attempt; Twilio allows 5-600
  callerId: process.env.TWILIO_NUMBER,      // unset: your phones see the caller's number
};

const VOICE = {
  en: { voice: 'Polly.Joanna-Neural', language: 'en-US' },
  fr: { voice: 'Polly.Gabrielle-Neural', language: 'fr-CA' },
};

const TEXT = {
  en: {
    closed: 'Thank you for calling. We are closed right now. Please leave a message after the tone.',
    menu: 'For sales, press 1 or say sales. For support, press 2 or say support. ' +
          'For our hours, press 3. To leave a message, press 0.',
    hours: 'We are open Monday to Thursday from 8:30 a.m. to 5 p.m., and Friday until 4 p.m., Eastern time.',
    invalid: 'Sorry, that is not one of the options.',
    noInput: 'Sorry, I did not hear a choice.',
    connecting: 'Connecting you now.',
    voicemail: 'Nobody is available. Please leave your name, number and a short message after the tone, then hang up.',
    thanks: 'Thank you. Goodbye.',
    hints: 'sales, support, hours, message',
  },
  fr: {
    closed: 'Merci de votre appel. Nos bureaux sont fermés. Laissez-nous un message après le signal sonore.',
    menu: 'Pour les ventes, faites le 1 ou dites ventes. Pour le soutien technique, faites le 2 ou dites soutien. ' +
          'Pour nos heures d’ouverture, faites le 3. Pour laisser un message, faites le 0.',
    hours: 'Nous sommes ouverts du lundi au jeudi de 8 h 30 à 17 h, et le vendredi jusqu’à 16 h, heure de l’Est.',
    invalid: 'Désolé, ce choix n’est pas valide.',
    noInput: 'Désolé, je n’ai pas entendu votre choix.',
    connecting: 'Nous vous mettons en communication.',
    voicemail: 'Personne n’est disponible. Laissez votre nom, votre numéro et un court message après le signal sonore, puis raccrochez.',
    thanks: 'Merci. Au revoir.',
    hints: 'ventes, soutien, heures, message',
  },
};

// ---- 2. Business hours, evaluated in the business's time zone ----
const fmt = new Intl.DateTimeFormat('en-CA', {
  timeZone: CONFIG.timeZone, weekday: 'short', year: 'numeric', month: '2-digit',
  day: '2-digit', hour: '2-digit', minute: '2-digit', hourCycle: 'h23',
});

function isOpen(now = new Date()) {
  const p = Object.fromEntries(fmt.formatToParts(now).map((x) => [x.type, x.value]));
  if (CONFIG.holidays.has(`${p.year}-${p.month}-${p.day}`)) return false;
  const span = CONFIG.hours[p.weekday];
  if (!span) return false;
  const hhmm = `${p.hour}:${p.minute}`;        // zero-padded, so string compare works
  return hhmm >= span[0] && hhmm < span[1];
}

// ---- 3. Helpers ----
const lang = (req) => (req.query.lang === 'fr' ? 'fr' : 'en');
const say = (node, l, text) => node.say(VOICE[l], text);
const send = (res, twiml) => res.type('text/xml').send(twiml.toString());

const app = express();
app.set('trust proxy', true);                   // ngrok / load balancer terminates TLS
app.use(express.urlencoded({ extended: false })); // Twilio posts form-encoded bodies
// Reject anything not signed by Twilio with your auth token (TWILIO_AUTH_TOKEN).
const fromTwilio = twilio.webhook({ validate: process.env.NODE_ENV !== 'test' });

// ---- 4. Entry point: the number's "A call comes in" webhook ----
app.post('/voice', fromTwilio, (req, res) => {
  const twiml = new VoiceResponse();
  if (!isOpen()) {
    say(twiml, 'en', TEXT.en.closed);
    say(twiml, 'fr', TEXT.fr.closed);
    recordVoicemail(twiml, 'en');
    return send(res, twiml);
  }
  // Bilingual greeting, then the English menu. 9 switches to French.
  const g = twiml.gather({
    input: 'dtmf', numDigits: 1, timeout: 4, action: '/language', actionOnEmptyResult: true,
  });
  say(g, 'en', 'Thank you for calling.');
  say(g, 'fr', 'Pour le service en français, faites le 9.');
  return send(res, twiml);
});

app.post('/language', fromTwilio, (req, res) => {
  const twiml = new VoiceResponse();
  twiml.redirect(req.body.Digits === '9' ? '/menu?lang=fr' : '/menu?lang=en');
  send(res, twiml);
});

// ---- 5. The menu itself ----
app.post('/menu', fromTwilio, (req, res) => {
  const l = lang(req);
  const tries = Number(req.query.tries || 0);
  const twiml = new VoiceResponse();
  const g = twiml.gather({
    input: 'dtmf speech', numDigits: 1, timeout: 5, speechTimeout: 'auto',
    language: VOICE[l].language, hints: TEXT[l].hints,
    action: `/menu/choice?lang=${l}&tries=${tries}`, actionOnEmptyResult: true,
  });
  say(g, l, TEXT[l].menu);
  send(res, twiml);
});

const SPOKEN = {
  1: /\b(sales|ventes?)\b/i, 2: /\b(support|soutien)\b/i,
  3: /\b(hours|heures?)\b/i, 0: /\b(message|messagerie)\b/i,
};

function choiceFrom(body) {
  if (body.Digits) return body.Digits;
  const heard = body.SpeechResult || '';
  return Object.keys(SPOKEN).find((k) => SPOKEN[k].test(heard)) || (heard ? 'unknown' : '');
}

app.post('/menu/choice', fromTwilio, (req, res) => {
  const l = lang(req);
  const tries = Number(req.query.tries || 0);
  const twiml = new VoiceResponse();
  const choice = choiceFrom(req.body);

  switch (choice) {
    case '1': {                                 // sales: simultaneous ring
      say(twiml, l, TEXT[l].connecting);
      const d = twiml.dial({
        timeout: CONFIG.ringSeconds, callerId: CONFIG.callerId,
        action: `/dial/done?lang=${l}`, answerOnBridge: true,
      });
      for (const n of CONFIG.sales) d.number({ url: `/screen?lang=${l}` }, n);
      return send(res, twiml);
    }
    case '2':                                   // support: one after the other
      say(twiml, l, TEXT[l].connecting);
      twiml.redirect(`/support?lang=${l}&i=0`);
      return send(res, twiml);
    case '3':
      say(twiml, l, TEXT[l].hours);
      twiml.redirect(`/menu?lang=${l}`);
      return send(res, twiml);
    case '0':
      twiml.redirect(`/voicemail/record?lang=${l}`);
      return send(res, twiml);
    default: {                                  // nothing, or a key we do not use
      if (tries >= 2) {                         // third failure: take a message
        twiml.redirect(`/voicemail/record?lang=${l}`);
        return send(res, twiml);
      }
      say(twiml, l, choice ? TEXT[l].invalid : TEXT[l].noInput);
      twiml.redirect(`/menu?lang=${l}&tries=${tries + 1}`);
      return send(res, twiml);
    }
  }
});

// ---- 6. Sequential fallback: ring support[i], and on failure try support[i+1] ----
app.post('/support', fromTwilio, (req, res) => {
  const l = lang(req);
  const i = Number(req.query.i || 0);
  const twiml = new VoiceResponse();
  if (i >= CONFIG.support.length) {
    twiml.redirect(`/voicemail/record?lang=${l}`);
    return send(res, twiml);
  }
  const d = twiml.dial({
    timeout: CONFIG.ringSeconds, callerId: CONFIG.callerId,
    action: `/support/next?lang=${l}&i=${i}`,
  });
  d.number({ url: `/screen?lang=${l}` }, CONFIG.support[i]);
  send(res, twiml);
});

app.post('/support/next', fromTwilio, (req, res) => {
  const l = lang(req);
  const i = Number(req.query.i || 0);
  const twiml = new VoiceResponse();
  if (req.body.DialBridged === 'true') twiml.hangup();   // they talked; we are done
  else twiml.redirect(`/support?lang=${l}&i=${i + 1}`);
  send(res, twiml);
});

app.post('/dial/done', fromTwilio, (req, res) => {
  const l = lang(req);
  const twiml = new VoiceResponse();
  if (req.body.DialBridged === 'true') twiml.hangup();
  else twiml.redirect(`/voicemail/record?lang=${l}`);
  send(res, twiml);
});

// ---- 7. Call screening: runs on the answering phone before the caller is connected.
// Stops a mobile's own voicemail from "answering" and swallowing the call.
app.post('/screen', fromTwilio, (req, res) => {
  const twiml = new VoiceResponse();
  const g = twiml.gather({ numDigits: 1, timeout: 5, action: '/screen/accept' });
  g.say(VOICE.en, `Business line call${req.query.lang === 'fr' ? ', in French' : ''}. Press 1 to accept.`);
  twiml.hangup();                               // no key pressed: release this leg
  send(res, twiml);
});

app.post('/screen/accept', fromTwilio, (req, res) => {
  const twiml = new VoiceResponse();
  if (req.body.Digits !== '1') twiml.hangup();  // empty response = connect the caller
  send(res, twiml);
});

// ---- 8. Voicemail ----
app.post('/voicemail/record', fromTwilio, (req, res) => {
  const l = lang(req);
  const twiml = new VoiceResponse();
  say(twiml, l, TEXT[l].voicemail);
  recordVoicemail(twiml, l);
  send(res, twiml);
});

function recordVoicemail(twiml, l) {
  twiml.record({
    maxLength: 120, timeout: 5, finishOnKey: '#', playBeep: true,
    action: `/voicemail/done?lang=${l}`,          // runs when recording ends
    recordingStatusCallback: `/voicemail/ready?lang=${l}`, // runs when the file exists
    recordingStatusCallbackEvent: 'completed',
    // Twilio's built-in transcription is American English only.
    ...(l === 'en' ? { transcribe: true, transcribeCallback: '/voicemail/text' } : {}),
  });
}

app.post('/voicemail/done', fromTwilio, (req, res) => {
  const l = lang(req);
  const twiml = new VoiceResponse();
  say(twiml, l, TEXT[l].thanks);
  twiml.hangup();
  send(res, twiml);
});

// ---- 9. Voicemail to email: Twilio stores the audio, your code sends the mail ----
let transport;                                  // created on first use
const mailer = () => (transport ??= nodemailer.createTransport(process.env.SMTP_URL));

app.post('/voicemail/ready', fromTwilio, (req, res) => {
  res.status(204).end();                        // answer Twilio first, then do the slow part
  const { RecordingUrl, RecordingSid, RecordingDuration, CallSid } = req.body;
  if (req.body.RecordingStatus !== 'completed') return;
  emailVoicemail({ RecordingUrl, RecordingSid, RecordingDuration, CallSid, lang: lang(req) })
    .catch((err) => console.error('voicemail email failed', RecordingSid, err));
});

async function emailVoicemail({ RecordingUrl, RecordingSid, RecordingDuration, CallSid, lang: l }) {
  // Media URLs need HTTP Basic auth: an API key SID and secret.
  const auth = Buffer.from(`${process.env.TWILIO_API_KEY}:${process.env.TWILIO_API_SECRET}`).toString('base64');
  const audio = await fetch(`${RecordingUrl}.mp3`, { headers: { Authorization: `Basic ${auth}` } });
  if (!audio.ok) throw new Error(`recording fetch ${audio.status}`);
  const call = await twilioClient().calls(CallSid).fetch();   // who called
  await mailer().sendMail({
    from: process.env.VOICEMAIL_FROM,
    to: process.env.VOICEMAIL_TO,
    subject: `Voicemail (${l.toUpperCase()}) from ${call.from}, ${RecordingDuration}s`,
    text: `New voicemail from ${call.from}.\nRecording SID: ${RecordingSid}\n` +
          (l === 'en' ? 'A transcript follows in a separate email if Twilio produces one.\n' : ''),
    attachments: [{ filename: `${RecordingSid}.mp3`, content: Buffer.from(await audio.arrayBuffer()) }],
  });
}

app.post('/voicemail/text', fromTwilio, (req, res) => {
  res.status(204).end();
  if (req.body.TranscriptionStatus !== 'completed') return;
  mailer().sendMail({
    from: process.env.VOICEMAIL_FROM, to: process.env.VOICEMAIL_TO,
    subject: 'Voicemail transcript',
    text: `${req.body.TranscriptionText}\n\nRecording: ${req.body.RecordingUrl}`,
  }).catch((err) => console.error('transcript email failed', err));
});

let client;
function twilioClient() {
  client ??= twilio(process.env.TWILIO_API_KEY, process.env.TWILIO_API_SECRET,
    { accountSid: process.env.TWILIO_ACCOUNT_SID });
  return client;
}

if (require.main === module) app.listen(process.env.PORT || 3000);
module.exports = { app, isOpen };

Lo que imprime para una persona que llama en francés y presiona 1, tal como se capturó en nuestra prueba:

<Response>
  <Say voice="Polly.Gabrielle-Neural" language="fr-CA">Nous vous mettons en communication.</Say>
  <Dial timeout="18" action="/dial/done?lang=fr" answerOnBridge="true">
    <Number url="/screen?lang=fr">+15145550101</Number>
    <Number url="/screen?lang=fr">+15145550102</Number>
  </Dial>
</Response>

Las URL relativas como /screen?lang=fr se resuelven respecto de la URL de la solicitud actual; la documentación de <Redirect> muestra un ejemplo relativo,11 y <Gather> y <Dial> documentan su action como relativa o absoluta.78 Los 18 segundos de timbre son una decisión nuestra: es tiempo suficiente para que alguien llegue a un teléfono, y lo ideal es un tiempo de timbre menor que la demora antes de que los celulares de su equipo desvíen la llamada a su propio buzón de voz, para que el filtrado rara vez tenga que rechazar un buzón. Mida esa demora en los teléfonos reales; varía según el operador y la configuración del teléfono.

Lo que este ejemplo omite deliberadamente

Las llamadas de emergencia (este menú solo marca los números que usted configura, nunca el 911, pero lea nuestra página sobre llamadas de emergencia antes de poner teléfonos detrás de él), el almacenamiento de registros de llamadas, los reintentos cuando el servidor de correo no funciona, la transcripción en francés, las alertas cuando se activa la URL de respaldo, y los números y el registro en Estados Unidos y Canadá. Cada uno de esos puntos es una preocupación de producción, no del menú.

Cuánto cuesta operarlo

Las páginas de precios de voz de Twilio para EE. UU. y Canadá dicen ambas “Pricing current as of August 2026” (precios vigentes a agosto de 2026) y listan las mismas cifras para los conceptos que usa un menú telefónico.2526 La página de Canadá muestra los precios con signo de dólar pero, según la leemos, no nombra la moneda.

ConceptoPágina de EE. UU.25Página de Canadá26
Número local$1.15 al mes$1.15 al mes
Número gratuito$2.15 al mes$2.15 al mes
Recibir una llamada, número local$0.0085 por minuto$0.0085 por minuto
Recibir una llamada, número gratuito$0.0220 por minuto$0.0220 por minuto
Hacer una llamada (el tramo desviado)$0.0140 por minuto$0.0140 por minuto a Canadá (Yukón se lista aparte)
Grabación / almacenamiento$0.0025 por minuto / $0.0005 por minuto al mesigual
Transcripción$0.0500 por minutoigual
Texto a voz, Standard / Neural / Generative$0.0008 / $0.0032 / $0.0130 por 100 caracteresigual

Desplace la tabla hacia los lados para ver todas las columnas.

El texto a voz se cobra “in blocks of 100 characters” (en bloques de 100 caracteres) con un mínimo de 100 caracteres, y las voces básicas son gratuitas.5 Un ejemplo calculado solo con esos precios de lista: una persona llama a un número local, escucha un saludo neural de 250 caracteres y un menú de 200 caracteres, y luego habla con ventas cuatro minutos. Considere unos cinco minutos de tiempo entrante (menú más conversación), cuatro minutos del tramo desviado y cinco bloques de 100 caracteres de voz neural: aproximadamente 5 × $0.0085 + 4 × $0.0140 + 5 × $0.0032 ≈ $0.11. Una llamada desviada cuesta dos tramos, el concepto que la gente olvida. No se incluyen impuestos, cargos regulatorios ni nada que no esté en la página de precios, y los precios cambian: revise las páginas el día que construya.

Nuestra opinión: cuándo vale la pena construirlo

Construir un menú telefónico en Twilio es un buen uso del tiempo de un desarrollador cuando el menú tiene que hacer algo específico de su negocio: buscar a quien llama en su propio sistema, seguir un rol de guardias o formar parte de un producto existente. La plataforma está bien documentada, las primitivas de esta guía son estables y usted es dueño de cada decisión.

Es un mal trato cuando el requisito es el ordinario de esta guía: un saludo, algunas opciones, los teléfonos correctos sonando y los mensajes llegando al correo. Entonces se compromete a operar un pequeño servicio de telefonía mientras el número esté publicado: la URL de respaldo, la lista de días festivos de cada año, la conservación de grabaciones, la factura, los trámites de registro de sus números y una llamada de prueba después de cada cambio. Una operadora automática alojada o la función de menú de su proveedor de telefonía actual suele ser la mejor opción ahí; nuestro propio servicio es una de esas opciones. Si construye, construya primero las rutas de fallo. Un menú que maneja con elegancia el silencio, las teclas incorrectas y un teléfono sin respuesta vale más que uno con una cuarta opción.

Para la redacción del menú en sí, nuestros guiones para menús telefónicos (en inglés) y las plantillas de flujo de llamadas están escritos para pegarse en el bloque de configuración de arriba. Para más guías de Twilio, vea la sección de Twilio.

Lecturas recomendadas

Artículos de investigación más largos sobre este tema (en inglés), cada uno con sus fuentes y un PDF descargable.

Fuentes

Cada afirmación sobre normas o precios en esta página enlaza a su fuente primaria, a menudo en inglés. Las normas y los precios cambian: verifique antes de confiar en una cifra.

  1. Twilio Docs, TwiML for Programmable Voice: “Twilio’s request to your application” (parámetros de la solicitud, POST frente a GET, ejecución de arriba hacia abajo, verbos y atributos que distinguen mayúsculas). Consultado el 26 de octubre de 2026
    https://www.twilio.com/docs/voice/twiml
  2. Twilio Docs, IncomingPhoneNumber resource (voice_url, voice_fallback_url, voice_caller_id_lookup a US$0.01 por consulta). Consultado el 26 de octubre de 2026
    https://www.twilio.com/docs/phone-numbers/api/incomingphonenumber-resource
  3. Twilio Docs, Webhooks connection overrides (tiempos límite predeterminados; tiempo límite máximo estricto de 15 segundos en solicitudes HTTP relacionadas con llamadas). Consultado el 26 de octubre de 2026
    https://www.twilio.com/docs/usage/webhooks/webhooks-connection-overrides
  4. Twilio Docs, TwiML Voice: <Say> (atributos voice, language y loop y sus valores predeterminados; nombres de voces por proveedor; compatibilidad de voz e idioma). Consultado el 26 de octubre de 2026
    https://www.twilio.com/docs/voice/twiml/say
  5. Twilio Docs, Text-to-speech (tabla de voces con fr-CA y en-US, voces Generative en Public Beta, SSML limitado a en-US, límites de caracteres, bloques de facturación de 100 caracteres). Consultado el 26 de octubre de 2026
    https://www.twilio.com/docs/voice/twiml/say/text-speech
  6. Twilio Docs, TwiML Voice: <Play> (loop y digits, tipos MIME admitidos, caché, transcodificación). Consultado el 26 de octubre de 2026
    https://www.twilio.com/docs/voice/twiml/play
  7. Twilio Docs, TwiML Voice: <Gather> (tabla de atributos y valores predeterminados, actionOnEmptyResult, comportamiento sin entrada, precedencia de dtmf speech, speechTimeout y speechModel, idiomas del modelo phone_call, Digits/SpeechResult/Confidence). Consultado el 26 de octubre de 2026
    https://www.twilio.com/docs/voice/twiml/gather
  8. Twilio Docs, TwiML Voice: <Dial> (timeout de 30 s predeterminado, 5-600 s; comportamiento de action; callerId; answerOnBridge; timeLimit; sequential; DialCallStatus, DialBridged, DialCallSid, DialCallDuration). Consultado el 26 de octubre de 2026
    https://www.twilio.com/docs/voice/twiml/dial
  9. Twilio Docs, TwiML Voice: <Number> (hasta diez números simultáneos, gana el primero que contesta, el atributo url para el filtrado). Consultado el 26 de octubre de 2026
    https://www.twilio.com/docs/voice/twiml/number
  10. Twilio Docs, TwiML Voice: <Record> (tabla de atributos y valores predeterminados, ciclo sin action, grabación aún no disponible al momento de action, límites de la transcripción y parámetros de devolución de llamada). Consultado el 26 de octubre de 2026
    https://www.twilio.com/docs/voice/twiml/record
  11. Twilio Docs, TwiML Voice: <Redirect> (method predeterminado POST, verbos posteriores inalcanzables, URL relativas). Consultado el 26 de octubre de 2026
    https://www.twilio.com/docs/voice/twiml/redirect
  12. Twilio Docs, TwiML Voice: <Hangup> (sin atributos; contestar antes de colgar se cobra igualmente, a diferencia de <Reject>). Consultado el 26 de octubre de 2026
    https://www.twilio.com/docs/voice/twiml/hangup
  13. Twilio Docs, Recording resource (medios mediante .mp3/.wav, autenticación HTTP Basic en las URL de medios, formatos, parámetros de la devolución de llamada de estado, metadatos conservados 40 días después de eliminarlos). Consultado el 26 de octubre de 2026
    https://www.twilio.com/docs/voice/api/recording
  14. Twilio Docs, Webhooks security (X-Twilio-Signature, HMAC-SHA1 con el token de autenticación, uso del validador del SDK, HTTPS). Consultado el 26 de octubre de 2026
    https://www.twilio.com/docs/usage/webhooks/webhooks-security
  15. Twilio Docs, Secure your Express app by validating incoming Twilio requests (twilio.webhook(), TWILIO_AUTH_TOKEN, diferencia de URL con ngrok y proxy). Consultado el 26 de octubre de 2026
    https://www.twilio.com/docs/usage/tutorials/how-to-secure-your-express-app-by-validating-incoming-twilio-requests
  16. twilio-node source, src/webhooks/webhooks.ts (opciones del webhook url, host, protocol; cómo se reconstruye la URL a partir de la solicitud). rama main, consultado el 26 de octubre de 2026
    https://github.com/twilio/twilio-node/blob/main/src/webhooks/webhooks.ts
  17. Twilio Docs, Functions invocation (firma del controlador, context y event, comportamiento de callback, la ejecución termina tras callback). Consultado el 26 de octubre de 2026
    https://www.twilio.com/docs/serverless/functions-assets/functions/invocation
  18. Twilio Docs, Functions and Assets FAQ (límite de ejecución de 10 segundos, 30 invocaciones simultáneas, solo CommonJS). Consultado el 26 de octubre de 2026
    https://www.twilio.com/docs/serverless/functions-assets/faq
  19. Twilio Docs, Functions and Assets visibility (public, protected, private). Consultado el 26 de octubre de 2026
    https://www.twilio.com/docs/serverless/functions-assets/visibility
  20. Twilio, Serverless pricing (Studio y Functions; “Pricing current as of August 2026”). Consultado el 26 de octubre de 2026
    https://www.twilio.com/en-us/serverless/pricing
  21. Twilio Docs, Studio overview (qué es Studio; las tarifas de los servicios subyacentes no están incluidas). Consultado el 26 de octubre de 2026
    https://www.twilio.com/docs/studio
  22. Twilio Docs, Studio widget library (nombres de los widgets de voz). Consultado el 26 de octubre de 2026
    https://www.twilio.com/docs/studio/widget-library
  23. Twilio Docs, Studio REST API v2 Flow resource (definición JSON, revisión, creación y actualización desde JSON). Consultado el 26 de octubre de 2026
    https://www.twilio.com/docs/studio/rest-api/v2/flow
  24. Twilio Docs, Twilio CLI: work with webhooks (phone-numbers:update --voice-url, sin URL localhost, ngrok, inspector, advertencia de exposición). Consultado el 26 de octubre de 2026
    https://www.twilio.com/docs/twilio-cli/general-usage/work-with-webhooks
  25. Twilio, Programmable Voice pricing, United States (“Pricing current as of August 2026”). Consultado el 26 de octubre de 2026
    https://www.twilio.com/en-us/voice/pricing/us
  26. Twilio, Programmable Voice pricing, Canada (“Pricing current as of August 2026”). Consultado el 26 de octubre de 2026
    https://www.twilio.com/en-us/voice/pricing/ca
  27. MDN Web Docs, Intl.DateTimeFormat() constructor (opciones timeZone y hourCycle). Consultado el 26 de octubre de 2026
    https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat
  28. Canada Revenue Agency, Public holidays (lista de 2026; los feriados provinciales y territoriales difieren). Fecha de modificación 2026-01-06; consultado el 26 de octubre de 2026
    https://www.canada.ca/en/revenue-agency/services/tax/public-holidays.html
  29. U.S. Office of Personnel Management, Federal holidays (lista de 2026 bajo 5 U.S.C. 6103, para empleados federales). Consultado el 26 de octubre de 2026
    https://www.opm.gov/policy-data-oversight/pay-leave/federal-holidays/
  30. Nodemailer, Attachments (content como cadena, Buffer o flujo; href y httpHeaders). Consultado el 26 de octubre de 2026
    https://nodemailer.com/message/attachments
  31. twilio-node, package.json (versión 6.2.0; engines node >=20.0.0). rama main, consultado el 26 de octubre de 2026
    https://github.com/twilio/twilio-node/blob/main/package.json