vicigeeksimple guides
Browse
All guides

VICIdial & agents · Inbound routing

Build a VICIdial IVR call menu: press 1 for sales, press 2 for support

Set up a VICIdial call menu that greets callers, routes each digit to the right in-group, and never leaves anyone in dead air on a timeout or a wrong key.

Reader setup

Before you start

Run each step in order and move only when the outcome is confirmed.

  1. An inbound DID already routing calls into a working in-group, with agents logged in and taking calls from it.
  2. Admin login access to admin.php with permission to modify DIDs, call menus, and the Audio Store, plus the read-only database account from Create a read-only database account for safe VICIdial queries for the verification queries below.
  3. A short greeting script written down - what you want the caller to hear and which digit sends them where.
What you will prove
A DID that answers with a recorded menu, sends 1 to sales and 2 to support, and routes a silent caller or a wrong key to a real destination instead of dead air.
Safety boundary
Dial the DID yourself before anyone else does - once pressing nothing, once pressing a digit you did not configure - and confirm both land somewhere sane, not a dropped call.

Reader path

How to use this article

  • Use it when: You need a fixed sequence to make a deployment or configuration change now.
  • Expected result: Follow each step and verify the outcome before changing the next layer.
  • Start here: Start at the first section and complete every checkpoint before moving to the next.

01 / 08

What a VICIdial call menu actually does

Fast answer: go to Inbound → Call Menus, add a menu, give it a Menu Prompt (the greeting), set Menu Timeout, Menu Timeout Prompt, Menu Invalid Prompt and Menu Repeat, then add one row per digit in its Call Menu Options grid pointing at an in-group, another menu, an extension, a voicemail box or a hangup. Add TIMEOUT and INVALID rows to that same grid too, then open the DID and change Route from IN_GROUP to CALLMENU and pick the Menu ID - skip that last step and the DID keeps ringing the old destination.

In plain language: this guide assumes you already know what a DID, an in-group, a campaign and an agent are — see Make a phone number ring an agent: inbound DIDs and in-groups for those definitions. The two new terms here: a call menu sits between the DID and the in-group and asks the caller to press a digit before deciding where the call goes - that is an IVR, an interactive voice response menu - and the digit itself is a DTMF tone, short for dual-tone multi-frequency, the two-tone beep a phone sends when you press a key.

Two tables hold everything: vicidial_call_menu is one row per menu - the greeting, the timeout, the repeat count. vicidial_call_menu_options is one row per key you want to react to, including two rows that are not digits at all - one for silence and one for a wrong key.

Trace path · read left to right
01Carrier trunk delivers the call to a DID pattern whose Route is set to CALLMENU and whose Menu ID names the menu02The menu plays its Menu Prompt, then waits Menu Timeout seconds for one DTMF digit03The matching Call Menu Options row - or the TIMEOUT or INVALID row - sends the call to an in-group, another menu, an extension, a voicemail box, or a hangup

Visual walkthrough

Follow three real demo screens

Captured on an isolated VICIdial demo: Administration screens on September 24, 2026, and the idle Agent screen on August 11, 2026. Each caption states its own capture time, and every sanitized image helps you recognize a related screen; none proves that this article's call, command, or result occurred.
Step 1 · Find the DID

Start from DID routing

Sanitized VICIdial DID Listings page with no DID rows present on the isolated demo
Captured September 24, 2026 at 21:53:19 UTC on the authorized isolated demo. This is DID routing configuration; it does not prove that a call was routed or answered.
Step 2 · Open the receiving In-Group

Review the receiving In-Group

Sanitized VICIdial In-Groups Listings page showing only the fixture inbound test group
Captured September 24, 2026 at 21:53:21 UTC on the authorized isolated demo. Only the fixture in-group is shown; other in-groups are removed. It does not prove that a call was routed, queued, or answered.
Step 3 · Recognize the receiving workspace

See the idle Agent destination

Sanitized logged-in VICIdial Agent screen in an idle no-live-call state with blank customer fields
Captured August 11, 2026 at 16:25:04 UTC on the authorized isolated demo. This is a real logged-in idle Agent screen with session and system identifiers redacted. Customer fields are blank, and it does not prove a placed, answered, recorded, transferred, or completed call.

02 / 08

Step 1 — Record or upload the greeting audio

Open Admin → Audio Store to upload a file, or use your install's phone-based recorder if it has one. Whatever you upload lands in the audio_store_details table, which tracks audio_format, wav_format_details and a wav_asterisk_valid flag the system sets to GOOD, BAD, or blank/NA after it inspects the file. If a prompt will not play on a live call, that flag - not your ears - is the first thing to check.

The Menu Prompt field itself just takes a filename, no extension - VICIdial checks the store for a matching .wav or .gsm behind the scenes. admin.php will not let you save a call menu without something in that field either: the exact validation message is `Call Menu Prompt must be populated. Use sip-silence if you do not actually want to play a prompt`. If you genuinely want dead silence before the beep, type sip-silence literally; otherwise type your greeting's filename.

Nothing in the sources pins one required sample rate to the Menu Prompt field on screen. What is verifiable: VICIdial's own default text-to-speech voice is named Allison-8kHz in the vicidial_tts_prompts table, which tells you the sample rate this platform is built around. Record or export your greeting as an 8000 Hz mono WAV if your recorder gives you the choice, then let the Audio Store's own validity check confirm the file rather than assuming. Before you upload anything of your own, point the same kind of check at a file VICIdial ships on every install — generic_hold, its own hold prompt — to see what a passing result looks like on this build.

Check a file's format, using the shipped generic_hold as a worked example
#!/bin/bash# See what a passing format check looks like against VICIdial's own shipped prompt,# before you point this same pattern at a greeting file of your own.set -euo pipefail CANDIDATE=""for DIR in /usr/share/asterisk/sounds /var/lib/asterisk/sounds; do  FOUND=$(find "$DIR" -iname 'generic_hold.*' 2>/dev/null | head -1)  if [ -n "$FOUND" ]; then    CANDIDATE="$FOUND"    break  fidone if [ -z "$CANDIDATE" ]; then  echo "FAIL: generic_hold not found under /usr/share/asterisk/sounds or /var/lib/asterisk/sounds on this build"  exit 1fi file "$CANDIDATE"case "$CANDIDATE" in  *.wav)    if file "$CANDIDATE" | grep -qi "mono"; then      echo "PASS: $CANDIDATE reports a mono channel layout"    else      echo "WARN: $CANDIDATE may not be mono"    fi    ;;  *)    echo "NOTE: $CANDIDATE is not a .wav, so file cannot report a channel layout for it -- on this build generic_hold ships only as .gsm"    ;;esac 
Evidence · ViciBox 12 demo capture

Captured demo response · 2026-09-23 21:35 UTC. The displayed command is the command that ran; a safe subset label means it was filtered, redacted, or fixture-scoped. Replays only after you select Replay transcript.

Command output line: #!/bin/bash
/usr/share/asterisk/sounds/generic_hold.gsm: data
NOTE: /usr/share/asterisk/sounds/generic_hold.gsm is not a .wav, so file cannot report a channel layout for it -- on this build generic_hold ships only as .gsm
Before you run it
Run this on the VICIdial server to see what a passing check looks like against the shipped hold prompt, before you point the same script at your own greeting file.
Success looks like
The script finds generic_hold under /usr/share/asterisk/sounds (this build ships it only as .gsm) and prints its file type — that is the pattern to reuse once CANDIDATE points at your own filename.
Stop if
If generic_hold is not found under either sounds directory, this build uses a different path — check it before assuming your own upload is the problem.

03 / 08

Step 2 — Create the call menu and set its timeout and repeat behavior

Go to Inbound → Call Menus and click to add a new one. Enter a Menu ID (no spaces or special characters, 2 to 50 characters, and not one of the reserved words vicidial, vicidial-auto, general, globals, default, trunkinbound, loopback-no-log, monitor_exit or monitor), a Menu Name, and an Admin User Group, then submit - that creates the bare vicidial_call_menu row and drops you into the modify screen to finish it.

On the modify screen, set Menu Prompt to your greeting's filename, Menu Timeout to how many seconds to wait for the first digit (the column default is 10), Menu Timeout Prompt and Menu Invalid Prompt to filenames you want played on those two events (the column default for both is the literal string NONE, meaning nothing plays), and Menu Repeat to how many times to replay the whole prompt before giving up (default 0, meaning no repeat at all).

Track Calls in Real-Time Report and Tracking Group control whether this menu shows up in the live inbound reports; Log Key Press and Log Field let you record every digit a caller presses into a lead's custom field if you need that for compliance or analytics. Menu Time Check and Call Time apply a schedule to the menu itself, separate from any schedule on the in-group behind it.

Confirm the menu saved with the values you expect
SELECT menu_id, menu_name, menu_prompt, menu_timeout, menu_timeout_prompt, menu_invalid_prompt, menu_repeatFROM vicidial_call_menuWHERE menu_id = '<MENU_ID>'; 
Evidence · ViciBox 12 demo capture · demo values substituted

Captured demo response · 2026-09-24 21:55 UTC. The displayed command is the command that ran; a safe subset label means it was filtered, redacted, or fixture-scoped. Replays only after you select Replay transcript.

Command output line: SELECT menu_id, menu_name, menu_prompt, menu_timeout, menu_timeout_prompt, menu_invalid_prompt, menu_repeat FROM vicidial_call_menu WHERE menu_id = 'default---agent';
+-----------------+-----------------------------------------------------+-------------+--------------+---------------------+---------------------+-------------+
| menu_id | menu_name | menu_prompt | menu_timeout | menu_timeout_prompt | menu_invalid_prompt | menu_repeat |
+-----------------+-----------------------------------------------------+-------------+--------------+---------------------+---------------------+-------------+
| default---agent | agent phones restricted to only internal extensions | sip-silence | 20 | NONE | NONE | 0 |
+-----------------+-----------------------------------------------------+-------------+--------------+---------------------+---------------------+-------------+
Before you run it
Run this with mysql --defaults-extra-file=/etc/vicidial-readonly.cnf against the database host, right after you submit the modify screen.
Success looks like
One row comes back with the Menu Prompt, Menu Timeout, Menu Timeout Prompt, Menu Invalid Prompt and Menu Repeat values you just typed - the save took.
Stop if
No row means the Menu ID does not match what admin.php actually stored; a row with menu_timeout_prompt or menu_invalid_prompt still showing NONE means you skipped a field admin.php was happy to leave blank.

04 / 08

Step 3 — Map every digit, timeout and wrong key to a destination

Below the menu's own settings, the modify screen shows the Call Menu Options grid - one row per key. The Option column is not limited to 0 through 9: it also offers HASH and STAR for # and *, A through D for the extra DTMF tones most phones never send, and five special values that are not digits at all - TIMECHECK, TIMEOUT, INVALID, INVALID_2ND and INVALID_3RD.

Each row's Route decides what happens: CALLMENU sends the caller into another menu, INGROUP sends them into a queue, DID reuses another DID's routing, EXTENSION dials an extension in a context you name, PHONE rings a specific phone, VOICEMAIL or VMAIL_NO_INST drops them into a mailbox, HANGUP plays one last audio file and ends the call, and AGI hands the call to a script. Pick a route and the form grows the fields it needs - an in-group picker for INGROUP, a filename box for HANGUP, and so on.

TIMEOUT and INVALID are the pair this article keeps coming back to: give each of them a real Route, the same way you would give digit 1 a Route, and a caller who says nothing or presses a wrong key lands somewhere useful instead of hearing the Menu Timeout Prompt or Menu Invalid Prompt once and then dead air. INVALID_2ND and INVALID_3RD let you escalate - repeat the prompt on the first miss, and route to a live queue or voicemail on the third, instead of cycling forever.

A worked two-option menu
Menu ID: <MENU_ID>Menu Prompt: sales-support-greeting (Thanks for calling. Press 1 for sales, press 2 for support.)Menu Timeout: 7Menu Timeout Prompt: menu-please-choose-againMenu Invalid Prompt: menu-that-was-not-a-valid-choiceMenu Repeat: 2 Call Menu Options:1           -> INGROUP   -> SALESQUEUE2           -> INGROUP   -> SUPPORTQUEUETIMEOUT     -> VOICEMAIL -> 8200INVALID     -> VOICEMAIL -> 8200INVALID_3RD -> HANGUP    -> vm-goodbye 
Not executed · worksheet or reference text

This sample is a template or reading aid, not a terminal command. There is no output to show.

Before you run it
Lay this out on paper - or in a text file like this one - before you open admin.php, so every digit and every fallback has an owner.
Success looks like
Dial the DID: pressing 1 rings SALESQUEUE, pressing 2 rings SUPPORTQUEUE, staying silent through the original prompt plus both Menu Repeat replays drops into voicemail box 8200, and pressing a wrong digit the first time replays the invalid prompt, not silence.
Stop if
If pressing 1 does nothing, the option_value stored is not literally 1 - check for a stray space or the wrong DTMF token in the grid's Option dropdown.

05 / 08

Point the DID at the menu instead of the in-group

Open the DID (Inbound → DIDs) and change Route from IN_GROUP to CALLMENU. A Menu ID selector appears once you do; pick the menu you just built. Submit.

This takes effect on the next inbound call, not the ones already ringing - VICIdial regenerates the dialplan from vicidial_inbound_dids and vicidial_call_menu, it does not touch a call already in progress. The In-Group value already on that DID stops mattering the moment Route is CALLMENU; you can leave it set, since your menu's own Call Menu Options rows are what pick the in-group now.

Every DID row also carries a filter_action and a whole second filter_* set of columns for area-code or URL-based overrides - if this DID already has a filter configured, that filter can route to CALLMENU independently of the main Route, using its own filter_menu_id. Check both before you conclude the menu is not being reached.

Confirm this build's dialplan already reflects the menu
#!/bin/bash# Confirm the dialplan already reflects the menu you just pointed the DID at.MENU_ID="<MENU_ID>"asterisk -rx "dialplan show $MENU_ID" | head -20 
Evidence · ViciBox 12 demo capture · demo values substituted

Captured demo response · 2026-09-23 21:35 UTC. The displayed command is the command that ran; a safe subset label means it was filtered, redacted, or fixture-scoped. Replays only after you select Replay transcript.

Command output line: #!/bin/bash # Confirm the dialplan already reflects the menu you just pointed the DID at. MENU_ID="default---agent" asterisk -rx "dialplan show $MENU_ID" | head -20
[ Context 'default---agent' created by 'pbx_config' ]
'h' => 1. AGI(agi://127.0.0.1:4577/call_log--HVcauses--PRI-----NODEBUG-----${HANGUPCAUSE}-----${DIALSTATUS}-----${DIALEDTIME}-----${ANSWEREDTIME}-----${HANGUPCAUSE(${HANGUPCAUSE_KEYS()},tech)})) [extensions-vicidial.conf:53]
'i' => 1. Goto(s,4) [extensions-vicidial.conf:50]
2. Hangup() [extensions-vicidial.conf:51]
's' => 1. Answer() [extensions-vicidial.conf:40]
2. AGI(agi-VDAD_inbound_calltime_check.agi,-----NO-----default---agent-------------------------NO-----YES) [extensions-vicidial.conf:41]
3. Set(INVCOUNT=0) [extensions-vicidial.conf:42]
4. Background(sip-silence) [extensions-vicidial.conf:43]
5. WaitExten(20) [extensions-vicidial.conf:44]
't' => 1. Playback(vm-goodbye) [extensions-vicidial.conf:48]
2. Hangup() [extensions-vicidial.conf:49]
Include => 'vicidial-auto-internal' [pbx_config]
Include => 'vicidial-auto-phones' [pbx_config]
-= 4 extensions (10 priorities) in 1 context. =-
Before you run it
Run this on the Asterisk server itself, right after you submit the DID change.
Success looks like
dialplan show returns extensions for the menu's generated context — VICIdial already regenerated the dialplan when you saved the DID, so no manual reload is needed.
Stop if
An empty result means this menu's generated context has not appeared — recheck that the DID's Route is actually CALLMENU and pointed at this Menu ID, rather than triggering a manual reload.

06 / 08

Nest menus without creating a loop

A Route of CALLMENU can point at any other menu_id, including one that itself has an option pointing back to the first menu. admin.php does not check for that when you save - the update to vicidial_call_menu_options writes whatever menu_id you picked from the dropdown, with no check for whether that menu, directly or three menus later, leads back to where the caller started.

Draw the tree before you build it: list every menu_id, every digit on it, and where that digit goes, on paper first. Give every sub-menu an exit - commonly 9 or star routing back to the parent menu or straight to an in-group - and never let that exit option point at a menu the caller has already been through.

A caller stuck in a two-menu loop does not get an error. They get the same greeting, over and over, until Menu Repeat runs out on whichever menu they are on when it happens - the same dead-air failure mode as an unset TIMEOUT row, just harder to spot in testing because the call sounds like it is doing something.

07 / 08

Troubleshoot: dead air, unreachable menus and loops

Most call-menu problems are one of a short list, and all of them are visible in the tables above without guessing.

If a live menu breaks, the fastest rollback is not to debug it with callers waiting - open the DID, set Route back to IN_GROUP, and pick the in-group that was working before. That takes effect on the next call and buys you time to fix the menu without leaving the number unanswered. Stop routing new calls into a broken menu the moment you know it is broken; fix it against your own test calls, then switch the DID back.

  • DID still rings the old in-group after you saved CALLMENU - check did_route and menu_id directly; a stale browser tab can resubmit the old value.
  • Caller hits silence after saying nothing - Menu Timeout Prompt is still the default NONE, or there is no TIMEOUT row in Call Menu Options.
  • Caller presses a key and nothing happens - the Option grid stores HASH and STAR, not the literal characters # and * as typed.
  • Menu keeps repeating and never lets go - Menu Repeat is set high with no eventual route on TIMEOUT, INVALID or INVALID_3RD.
  • A nested menu never lets anyone out - a sub-menu's exit option points at another menu deeper in the tree instead of back to an in-group.
  • A DID that suddenly rings nowhere is not always a menu problem - a carrier failure or a dead trunk upstream looks identical to a broken CALLMENU route until you check the trunk.

08 / 08

What comes after the menu answers correctly

A call menu only decides which door a caller walks through. What happens once they are queued behind an in-group - hold music, how long they wait before overflowing, what happens after hours - is a separate set of settings on the in-group itself.

The next article in this series is Tune a VICIdial inbound queue: hold music, wait time, overflow and after hours.

Inbound calling with VICIdial

Part 2 of 4

Follow without guesswork

Get the next article

RSS is live now. Email delivery below is an explicit local preview and sends nothing.Open the RSS feed
Email preview only. The address stays in this browser and is never transmitted.