How to Build an IVR Menu in 2025 (Asterisk, FreeSWITCH, and Twilio)
A practical guide to building IVR menus from scratch — covering dialplan design, prompt recording, DTMF handling, speech recognition, and integration with your CRM or help desk.
How to Build an IVR Menu in 2025
An IVR (Interactive Voice Response) menu is the front door of your phone system. Done well, it routes callers to the right place quickly. Done poorly, it drives customers to hang up.
This guide covers how to build a production-grade IVR from scratch using Asterisk, FreeSWITCH, or Twilio — including the common mistakes that make IVRs frustrating.
IVR Design Principles Before You Write a Line of Code
Before touching configuration, get your call flow on paper:
1. Map the customer intent, not your org chart — Don't build the menu around your departments. Build it around why customers call. "Track my order" is better than "Operations department."
2. Keep menus shallow — Maximum 4–5 options per level. Maximum 2 levels deep. Callers don't remember option 6.
3. Always offer escape routes — "Press 0 for an agent" should be available at every level.
4. Read options before the key — Say "For billing, press 2" not "Press 2 for billing." Callers listen for their intent, then remember the key.
5. Time out gracefully — If the caller doesn't press anything, don't just replay the menu. Offer to connect them with an agent.
Building an IVR with Asterisk Dialplan
Asterisk uses a dialplan in /etc/asterisk/extensions.conf. Here's a basic IVR:
[ivr-main]
; Answer the call
exten => s,1,Answer()
same => n,Wait(1)
; Play the main menu greeting
same => n,Background(ivr/main-menu)
same => n,WaitExten(5)
; Timeout — offer agent
exten => t,1,Goto(agent-queue,s,1)
; Invalid input
exten => i,1,Playback(ivr/invalid-option)
same => n,Goto(ivr-main,s,3)
; Option 1: Sales
exten => 1,1,Playback(ivr/connecting-sales)
same => n,Goto(sales-queue,s,1)
; Option 2: Support
exten => 2,1,Playback(ivr/connecting-support)
same => n,Goto(support-queue,s,1)
; Option 3: Billing
exten => 3,1,Playback(ivr/connecting-billing)
same => n,Goto(billing-queue,s,1)
; Option 0: Agent
exten => 0,1,Goto(agent-queue,s,1)The Background() application plays a sound file while listening for DTMF input. WaitExten(5) gives the caller 5 seconds to press a key before triggering the timeout extension.
Recording Prompts
Record prompts at 8kHz, 16-bit mono (the native format for PSTN calls):
# Using SoX to convert a studio recording to Asterisk format
sox input.wav -r 8000 -c 1 -e signed-integer -b 16 output.wavPlace recordings in /var/lib/asterisk/sounds/ivr/. Reference them without the extension in the dialplan: Background(ivr/main-menu) plays /var/lib/asterisk/sounds/ivr/main-menu.wav.
Building an IVR with FreeSWITCH
FreeSWITCH uses Lua, JavaScript, or XML macros for IVR logic. Here's a Lua IVR script:
-- /usr/share/freeswitch/scripts/ivr_main.lua
session:answer()
session:sleep(1000)
local choice = session:playAndGetDigits(
1, -- min digits
1, -- max digits
3, -- max tries
5000, -- timeout (ms)
"#", -- terminator
"ivr/main-menu", -- prompt file
"ivr/invalid-option", -- bad input file
"1-3|0" -- valid digits regex
)
if choice == "1" then
session:transfer("sales_queue", "XML", "default")
elseif choice == "2" then
session:transfer("support_queue", "XML", "default")
elseif choice == "3" then
session:transfer("billing_queue", "XML", "default")
elseif choice == "0" then
session:transfer("agent_queue", "XML", "default")
else
session:transfer("agent_queue", "XML", "default")
endplayAndGetDigits handles the prompt playback, retry logic, and input collection in one call — simpler than the equivalent Asterisk dialplan.
Building an IVR with Twilio
Twilio uses TwiML (XML) served from your web endpoint. When a call arrives, Twilio makes an HTTP request to your URL and executes the TwiML response:
<?xml version="1.0" encoding="UTF-8"?>
<Response>
<Gather numDigits="1" action="/ivr/handle-input" timeout="5">
<Say voice="Polly.Joanna">
Thank you for calling Voxmation.
For sales, press 1.
For support, press 2.
For billing, press 3.
To speak with an agent, press 0.
</Say>
</Gather>
<!-- No input fallback -->
<Redirect>/ivr/agent</Redirect>
</Response>Your /ivr/handle-input endpoint receives the Digits POST parameter and returns the appropriate TwiML:
// Express.js handler
app.post('/ivr/handle-input', (req, res) => {
const digit = req.body.Digits;
const queueMap = {
'1': 'sales',
'2': 'support',
'3': 'billing',
'0': 'agent',
};
const queue = queueMap[digit] || 'agent';
res.type('text/xml');
res.send(`
<?xml version="1.0" encoding="UTF-8"?>
<Response>
<Enqueue>${queue}</Enqueue>
</Response>
`);
});Adding Speech Recognition to Your IVR
DTMF-only IVRs frustrate mobile callers who are driving or have accessibility needs. Adding speech recognition lets callers say their intent instead of pressing keys.
With Twilio, add the input attribute to :
<Gather input="speech dtmf" numDigits="1" action="/ivr/handle-input" timeout="5" speechTimeout="auto">
<Say>How can I help you today? You can say Sales, Support, or Billing — or press 1, 2, or 3.</Say>
</Gather>The SpeechResult POST parameter will contain the transcribed input alongside Digits.
Connecting Your IVR to a CRM
The most valuable IVR integration is CRM screen-pop: when an inbound call arrives, look up the caller's number, retrieve their account, and surface it to the agent before they pick up.
With Asterisk AGI:
#!/usr/bin/env python3
import sys, requests
agi_vars = {}
for line in sys.stdin:
line = line.strip()
if not line:
break
key, _, value = line.partition(': ')
agi_vars[key] = value
caller_id = agi_vars.get('agi_callerid', '')
resp = requests.get(
f'https://your-crm/v1/contacts/search?phone={caller_id}',
headers={'Authorization': 'Bearer API_KEY'},
timeout=2
)
if resp.ok:
contact = resp.json()
print(f'SET VARIABLE CONTACT_NAME {contact["name"]}')
print(f'SET VARIABLE CONTACT_ID {contact["id"]}')
sys.stdout.flush()With Twilio, make a server-side API call in your TwiML webhook handler before returning the response.
Common IVR Mistakes to Avoid
1. No timeout handling — Always handle the case where the caller doesn't press anything.
2. Too many options — More than 5 options per level, and callers start hanging up.
3. Long intros — "Thank you for calling Voxmation, a leader in voice prompt automation solutions" wastes 8 seconds. Say "Thank you for calling Voxmation" and get to the menu.
4. Dead ends — Every path should either complete the customer's goal or route to an agent. No dead ends.
5. No error recovery — After invalid input, play "I didn't catch that" and try again — up to 3 times. Then route to an agent.
6. Using TTS for all prompts — TTS has improved dramatically but still sounds robotic for key messages. Record a human voice for your greeting. Use TTS for dynamic content (account numbers, appointment times).
A well-built IVR handles the majority of inbound calls without a live agent, reduces hold times, and improves caller satisfaction. The technology is proven — the results depend entirely on the call flow design and prompt quality.
Ready to automate your voice interactions?
Voxmation makes it easy to build IVR menus, appointment reminders, and outbound campaigns — on your infrastructure or ours.
