← All Tutorials

ViciDial IVR Setup with Asterisk — Inbound Call Menus

ViciDial Administration Intermediate 11 min read #49

Master building production-grade interactive voice response (IVR) systems in ViciDial using Asterisk dialplans, call routing, and real-time menu logic

Prerequisites

Before starting this tutorial, ensure you have:

Understanding ViciDial IVR Architecture

ViciDial's IVR system sits at the intersection of Asterisk dialplans and the ViciDial database. When an inbound call arrives, it follows this path:

  1. DID routes to an Asterisk context (usually defined in extensions-vicidial.conf)
  2. IVR dialplan plays prompts and collects DTMF input
  3. Digits trigger logic that queries ViciDial database or executes AGI scripts
  4. Calls route to campaigns, queues, or agents based on menu selections
  5. Call metadata logs to vicidial_log table

The key difference from traditional Asterisk IVR: ViciDial maintains call state in its database, enabling features like call recording, agent integration, and reporting.

Section 1: Core IVR Dialplan Structure

Creating the Main IVR Context

Your primary IVR context lives in /etc/asterisk/extensions-vicidial.conf. Here's a production-tested template:

[from-vicidial-ivr]
; Main IVR entry point for inbound DID calls
exten => s,1,Answer()
exten => s,n,Set(CALLFILENAME=${UNIQUEID})
exten => s,n,Set(CHANNEL(language)=en)
exten => s,n,NoOp(=== ViciDial IVR Entry - CallID: ${UNIQUEID} ===)
exten => s,n,Set(vicidial_log_id=${SHELL(/usr/share/astguiclient/VICIDIAL_log_insert.agi ${CHANNEL(name)} ${EXTEN} ${CALLERID(num)} ${CALLERID(name)})})
exten => s,n,Goto(ivr_main_menu,s,1)

[ivr_main_menu]
; Main menu - play prompt and collect input
exten => s,1,NoOp(=== Main Menu ===)
exten => s,n,Set(TIMEOUT(digit)=5)
exten => s,n,Set(TIMEOUT(response)=10)
exten => s,n,Background(/var/spool/asterisk/monolithic/recordings/main_menu_prompt)
exten => s,n,WaitExten(2)

; Menu options - route based on DTMF input
exten => 1,1,Goto(sales_queue,s,1)
exten => 2,1,Goto(support_queue,s,1)
exten => 3,1,Goto(billing_queue,s,1)
exten => 0,1,Goto(operator_queue,s,1)

; Default handler - timeout or invalid input
exten => t,1,Playback(invalid_selection)
exten => t,n,Goto(ivr_main_menu,s,1)

exten => i,1,Playback(invalid_selection)
exten => i,n,Goto(ivr_main_menu,s,1)

; Repeat menu option
exten => *,1,Goto(ivr_main_menu,s,1)

[sales_queue]
exten => s,1,NoOp(=== Routing to Sales Queue ===)
exten => s,n,Set(CAMPAIGN_ID=sales_inbound)
exten => s,n,Queue(sales_queue,t,,,300)
exten => s,n,Playback(sorry_all_agents_busy)
exten => s,n,VoiceMail(u1000@vicidial)
exten => s,n,Hangup()

[support_queue]
exten => s,1,NoOp(=== Routing to Support Queue ===)
exten => s,n,Set(CAMPAIGN_ID=support_inbound)
exten => s,n,Queue(support_queue,t,,,300)
exten => s,n,Playback(sorry_all_agents_busy)
exten => s,n,VoiceMail(u1001@vicidial)
exten => s,n,Hangup()

[billing_queue]
exten => s,1,NoOp(=== Routing to Billing Queue ===)
exten => s,n,Set(CAMPAIGN_ID=billing_inbound)
exten => s,n,Queue(billing_queue,t,,,300)
exten => s,n,Playback(sorry_all_agents_busy)
exten => s,n,VoiceMail(u1002@vicidial)
exten => s,n,Hangup()

[operator_queue]
exten => s,1,NoOp(=== Routing to Operator Queue ===)
exten => s,n,Set(CAMPAIGN_ID=operator_inbound)
exten => s,n,Queue(operator_queue,t,,,300)
exten => s,n,Hangup()

Connecting DID to IVR Context

In your inbound route configuration (via Asterisk or ViciDial UI), point the DID to the from-vicidial-ivr context:

[from-trunk]
; Receives calls from SIP trunk
exten => 1234567890,1,Goto(from-vicidial-ivr,s,1)

Alternatively, configure this in /etc/asterisk/sip.conf:

[trunk-provider]
type=peer
host=your.sip.provider
context=from-trunk
insecure=port,invite

Section 2: Database-Driven Menu Logic

Storing Menu Configurations in ViciDial Database

Instead of hardcoding menus in dialplans, use the ViciDial database to define dynamic menus. Create a custom table:

CREATE TABLE vicidial_ivr_menus (
    menu_id INT AUTO_INCREMENT PRIMARY KEY,
    menu_name VARCHAR(50) UNIQUE NOT NULL,
    campaign_id VARCHAR(20),
    prompt_filename VARCHAR(255),
    timeout_seconds INT DEFAULT 10,
    max_attempts INT DEFAULT 3,
    created_date DATETIME DEFAULT CURRENT_TIMESTAMP,
    modified_date DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    active INT DEFAULT 1
) ENGINE=InnoDB;

CREATE TABLE vicidial_ivr_options (
    option_id INT AUTO_INCREMENT PRIMARY KEY,
    menu_id INT NOT NULL,
    digit_pressed VARCHAR(2),
    action_type ENUM('goto_menu', 'queue', 'voicemail', 'hangup', 'agi_script'),
    action_target VARCHAR(100),
    description VARCHAR(100),
    sequence INT,
    FOREIGN KEY (menu_id) REFERENCES vicidial_ivr_menus(menu_id),
    UNIQUE KEY unique_menu_digit (menu_id, digit_pressed)
) ENGINE=InnoDB;

Insert sample menu configuration:

INSERT INTO vicidial_ivr_menus (menu_name, campaign_id, prompt_filename, timeout_seconds, max_attempts) VALUES
('main_menu', 'ivr_main', '/var/spool/asterisk/monolithic/recordings/main_menu', 10, 3),
('billing_submenu', 'billing_inbound', '/var/spool/asterisk/monolithic/recordings/billing_menu', 8, 2);

INSERT INTO vicidial_ivr_options (menu_id, digit_pressed, action_type, action_target, description, sequence) VALUES
(1, '1', 'queue', 'sales_queue', 'Sales Department', 1),
(1, '2', 'queue', 'support_queue', 'Support Department', 2),
(1, '3', 'goto_menu', 'billing_submenu', 'Billing', 3),
(2, '1', 'queue', 'billing_queue', 'Billing Support', 1),
(2, '2', 'voicemail', 'u1002@vicidial', 'Leave Message', 2);

AGI Script for Dynamic Menu Routing

Create /var/lib/asterisk/agi-bin/vicidial_ivr_router.agi:

#!/usr/bin/perl
# ViciDial IVR Router - Dynamic menu handling via database

use strict;
use Asterisk::AGI;
use DBI;
use Data::Dumper;

my $agi = new Asterisk::AGI;

# Database configuration
my $dbhost = 'localhost';
my $dbname = 'asterisk';
my $dbuser = 'vicidial';
my $dbpass = 'your_db_password';

my $dbh = DBI->connect("DBI:mysql:$dbname:$dbhost", $dbuser, $dbpass)
    or die "Cannot connect: $DBI::errstr\n";

# Get variables from Asterisk
my $menu_name = $agi->get_variable('MENU_NAME');
my $digit_pressed = $agi->get_variable('DIGIT_PRESSED');
my $callid = $agi->get_variable('UNIQUEID');

$agi->verbose("IVR Router: Menu=$menu_name, Digit=$digit_pressed", 3);

# Fetch menu configuration
my $menu_query = $dbh->prepare(
    "SELECT menu_id, prompt_filename, timeout_seconds FROM vicidial_ivr_menus WHERE menu_name = ? AND active = 1"
);
$menu_query->execute($menu_name) or die "Query failed: $DBI::errstr\n";
my $menu = $menu_query->fetchrow_hashref();

unless ($menu) {
    $agi->exec('Playback', 'invalid_selection');
    $agi->set_variable('MENU_ACTION', 'retry');
    $dbh->disconnect();
    exit(0);
}

# Fetch option for pressed digit
my $option_query = $dbh->prepare(
    "SELECT action_type, action_target FROM vicidial_ivr_options WHERE menu_id = ? AND digit_pressed = ?"
);
$option_query->execute($menu->{menu_id}, $digit_pressed) or die "Query failed: $DBI::errstr\n";
my $option = $option_query->fetchrow_hashref();

if ($option) {
    $agi->verbose("Executing action: $option->{action_type} -> $option->{action_target}", 3);
    
    if ($option->{action_type} eq 'queue') {
        $agi->set_variable('QUEUE_NAME', $option->{action_target});
        $agi->set_variable('MENU_ACTION', 'queue');
    } elsif ($option->{action_type} eq 'goto_menu') {
        $agi->set_variable('MENU_NAME', $option->{action_target});
        $agi->set_variable('MENU_ACTION', 'goto_menu');
    } elsif ($option->{action_type} eq 'voicemail') {
        $agi->set_variable('VOICEMAIL_BOX', $option->{action_target});
        $agi->set_variable('MENU_ACTION', 'voicemail');
    } elsif ($option->{action_type} eq 'hangup') {
        $agi->set_variable('MENU_ACTION', 'hangup');
    }
} else {
    $agi->verbose("No action defined for digit: $digit_pressed", 2);
    $agi->set_variable('MENU_ACTION', 'invalid');
}

$dbh->disconnect();
exit(0);

Make the script executable:

chmod 755 /var/lib/asterisk/agi-bin/vicidial_ivr_router.agi
chown asterisk:asterisk /var/lib/asterisk/agi-bin/vicidial_ivr_router.agi

Dialplan Integration with AGI

Update your main dialplan to use the AGI script:

[ivr_dynamic_menu]
exten => s,1,NoOp(=== Dynamic Menu Handler ===)
exten => s,n,Set(TIMEOUT(digit)=5)
exten => s,n,Set(TIMEOUT(response)=${timeout_seconds})
exten => s,n,Background(${prompt_filename})
exten => s,n,WaitExten(2)

exten => 1,1,Set(DIGIT_PRESSED=1)
exten => 1,n,AGI(vicidial_ivr_router.agi)
exten => 1,n,GotoIf($["${MENU_ACTION}"="queue"]?execute_queue,s,1)
exten => 1,n,GotoIf($["${MENU_ACTION}"="goto_menu"]?ivr_dynamic_menu,s,1)
exten => 1,n,GotoIf($["${MENU_ACTION}"="voicemail"]?execute_voicemail,s,1)
exten => 1,n,Hangup()

exten => 2,1,Set(DIGIT_PRESSED=2)
exten => 2,n,AGI(vicidial_ivr_router.agi)
exten => 2,n,GotoIf($["${MENU_ACTION}"="queue"]?execute_queue,s,1)
exten => 2,n,GotoIf($["${MENU_ACTION}"="goto_menu"]?ivr_dynamic_menu,s,1)
exten => 2,n,Hangup()

; Repeat for digits 3-9...

exten => t,1,Playback(invalid_selection)
exten => t,n,Goto(ivr_dynamic_menu,s,1)

exten => i,1,Playback(invalid_selection)
exten => i,n,Goto(ivr_dynamic_menu,s,1)

[execute_queue]
exten => s,1,Queue(${QUEUE_NAME},t,,,300)
exten => s,n,Playback(all_agents_busy)
exten => s,n,Hangup()

[execute_voicemail]
exten => s,1,VoiceMail(${VOICEMAIL_BOX})
exten => s,n,Hangup()

Section 3: Call Recording and ViciDial Integration

Enabling Call Recording in IVR

ViciDial requires explicit recording setup. Add this to your IVR entry context:

[from-vicidial-ivr]
exten => s,1,Answer()
exten => s,n,Set(CALLFILENAME=${STRFTIME(${EPOCH},,%Y%m%d-%H%M%S)}-${UNIQUEID})
exten => s,n,Set(CHANNEL(language)=en)
exten => s,n,MixMonitor(/var/spool/asterisk/monolithic/recordings/ivr/${CALLFILENAME}.wav,b)
exten => s,n,Set(RECORDING_FILE=/var/spool/asterisk/monolithic/recordings/ivr/${CALLFILENAME}.wav)
exten => s,n,NoOp(=== ViciDial IVR Recording: ${RECORDING_FILE} ===)
exten => s,n,Goto(ivr_main_menu,s,1)

Ensure the recording directory exists and has proper permissions:

mkdir -p /var/spool/asterisk/monolithic/recordings/ivr
chown asterisk:asterisk /var/spool/asterisk/monolithic/recordings/ivr
chmod 755 /var/spool/asterisk/monolithic/recordings/ivr

Logging IVR Events to ViciDial Database

Create a dialplan macro to log IVR interactions:

[macro-ivr_log_event]
; Arguments: ${ARG1}=event_type, ${ARG2}=menu_name, ${ARG3}=digit_pressed
exten => s,1,NoOp(=== Logging IVR Event ===)
exten => s,n,AGI(agi://<ivr_logger_script>,${ARG1},${ARG2},${ARG3})

[from-vicidial-ivr-logged]
exten => s,1,Answer()
exten => s,n,Set(CALLFILENAME=${UNIQUEID})
exten => s,n,MixMonitor(/var/spool/asterisk/monolithic/recordings/ivr/${CALLFILENAME}.wav,b)
exten => s,n,Macro(ivr_log_event,ivr_entry,main_menu,)
exten => s,n,Goto(ivr_main_menu,s,1)

[ivr_main_menu]
exten => s,1,Set(TIMEOUT(digit)=5)
exten => s,n,Set(TIMEOUT(response)=10)
exten => s,n,Background(/var/spool/asterisk/monolithic/recordings/main_menu_prompt)
exten => s,n,WaitExten(2)

exten => 1,1,Macro(ivr_log_event,digit_pressed,main_menu,1)
exten => 1,n,Goto(sales_queue,s,1)

exten => 2,1,Macro(ivr_log_event,digit_pressed,main_menu,2)
exten => 2,n,Goto(support_queue,s,1)

exten => t,1,Macro(ivr_log_event,timeout,main_menu,)
exten => t,n,Goto(ivr_main_menu,s,1)

exten => i,1,Macro(ivr_log_event,invalid_input,main_menu,)
exten => i,n,Goto(ivr_main_menu,s,1)

Section 4: Advanced Features — Caller Authentication and Call Routing

Account Lookup with DTMF Entry

Allow customers to enter an account number for routing:

[ivr_account_lookup]
exten => s,1,NoOp(=== Account Lookup ===)
exten => s,n,Set(TIMEOUT(digit)=5)
exten => s,n,Playback(enter_account_number)
exten => s,n,Read(ACCOUNT_NUMBER,please_enter_account)
exten => s,n,AGI(agi://<account_validation>,${ACCOUNT_NUMBER})
exten => s,n,GotoIf($["${ACCOUNT_VALID}"="1"]?lookup_success,s,1)
exten => s,n,Playback(account_not_found)
exten => s,n,Goto(ivr_main_menu,s,1)

[lookup_success]
exten => s,1,NoOp(=== Account Found: ${ACCOUNT_NUMBER} ===)
exten => s,n,Set(CAMPAIGN_ID=${ACCOUNT_CAMPAIGN})
exten => s,n,Queue(${ACCOUNT_QUEUE},t,,,300)
exten => s,n,Playback(sorry_all_agents_busy)
exten => s,n,Hangup()

IVR Call Priority Based on Menu Selection

Route calls with different queue priorities:

[ivr_priority_menu]
exten => s,1,NoOp(=== Priority Menu ===)
exten => s,n,Set(TIMEOUT(digit)=5)
exten => s,n,Set(TIMEOUT(response)=10)
exten => s,n,Background(/var/spool/asterisk/monolithic/recordings/priority_menu)
exten => s,n,WaitExten(2)

; High priority (existing customers)
exten => 1,1,Set(QUEUE_PRIORITY=10)
exten => 1,n,Set(CALL_TYPE=existing_customer)
exten => 1,n,Goto(route_to_queue,s,1)

; Standard priority (new customers)
exten => 2,1,Set(QUEUE_PRIORITY=5)
exten => 2,n,Set(CALL_TYPE=new_customer)
exten => 2,n,Goto(route_to_queue,s,1)

; VIP priority
exten => 3,1,Set(QUEUE_PRIORITY=15)
exten => 3,n,Set(CALL_TYPE=vip_customer)
exten => 3,n,Goto(route_to_queue,s,1)

[route_to_queue]
exten => s,1,NoOp(=== Queue with Priority ${QUEUE_PRIORITY} ===)
exten => s,n,Set(QUEUE_PRIORITY=${QUEUE_PRIORITY})
exten => s,n,Queue(sales_queue,t,,,300)
exten => s,n,Hangup()

Section 5: Troubleshooting IVR Issues

Common Problems and Solutions

IVR Not Receiving DTMF Input

Problem: Menu plays but pressing buttons doesn't work.

Diagnosis:

asterisk -rx "sip set debug on"
asterisk -rx "core set verbose 3"
tail -f /var/log/asterisk/messages | grep -i dtmf

Solution: Check for DTMF mode mismatch. ViciDial typically uses RFC2833 (RTP-based):

[sip-vicidial]
dtmfmode=rfc2833

Restart Asterisk:

systemctl restart asterisk

Calls Dropping After Menu Selection

Problem: Call disconnects after pressing a digit.

Diagnosis: Check dialplan syntax:

asterisk -rx "dialplan reload"
asterisk -rx "dialplan show from-vicidial-ivr" | grep -A5 "exten => 1"

Solution: Verify all Goto() statements reference valid contexts and extensions:

exten => 1,1,Goto(sales_queue,s,1)
; Verify that 'sales_queue' context exists with 's' extension

Recording Files Not Found

Problem: Playback fails with "file not found" error.

Diagnosis:

ls -la /var/spool/asterisk/monolithic/recordings/
asterisk -rx "core set verbose 3" | grep -i playback

Solution: Ensure audio files exist and are in correct format:

# Check format
file /var/spool/asterisk/monolithic/recordings/main_menu_prompt.wav

# Convert if necessary
sox input.wav -b 8 -r 8000 output.wav remix -

# Test playback
asterisk -rx "core set verbose 3"
# In dialplan: Playback(/var/spool/asterisk/monolithic/recordings/test_prompt)

IVR Context Not Loading

Problem: "Unknown context" error when dialing DID.

Diagnosis:

asterisk -rx "dialplan show from-vicidial-ivr"
asterisk -rx "core show functions" | grep Load

Solution: Reload dialplan and verify syntax:

# Check syntax first
asterisk -vvvvvc -g 2>&1 | head -50

# Reload
asterisk -rx "dialplan reload"

# Verify
asterisk -rx "dialplan show from-vicidial-ivr" | head -20

AGI Script Errors

Problem: AGI script returns error, calls fail.

Diagnosis:

tail -f /var/log/asterisk/messages | grep agi
asterisk -rx "agi debug on"

Solution: Check script permissions and Perl dependencies:

chmod 755 /var/lib/asterisk/agi-bin/vicidial_ivr_router.agi
perl -c /var/lib/asterisk/agi-bin/vicidial_ivr_router.agi
cpan install Asterisk::AGI

Queue Timeout Not Working

Problem: Calls queue indefinitely instead of timing out.

Diagnosis:

asterisk -rx "queue show sales_queue"

Solution: Verify queue timeout is set in dialplan:

exten => s,1,Queue(sales_queue,t,,,300)
;                                       ^^^ timeout in seconds

Also check queue definition in queues.conf:

[sales_queue]
member => SIP/agent1
member => SIP/agent2
timeout=20

Database Connection Failures in AGI

Problem: AGI script fails to connect to database.

Diagnosis:

mysql -h localhost -u vicidial -p asterisk -e "SELECT COUNT(*) FROM vicidial_ivr_menus;"

Solution: Verify credentials and permissions:

# From MySQL:
GRANT SELECT ON asterisk.vicidial_ivr_menus TO 'vicidial'@'localhost';
FLUSH PRIVILEGES;

# Test Perl DBI connection
perl -e 'use DBI; my $dbh = DBI->connect("DBI:mysql:asterisk:localhost", "vicidial", "password"); print "OK\n";'

Section 6: Monitoring and Testing Your IVR

Asterisk CLI Commands for IVR Debugging

Monitor active calls:

asterisk -rx "core show calls"

View queue statistics:

asterisk -rx "queue show sales_queue"

Monitor real-time verbose output:

asterisk -rx "core set verbose 3"
tail -f /var/log/asterisk/messages

Test dialplan routing:

asterisk -rx "dialplan eval 1234567890 from-vicidial-ivr s"

Check for syntax errors:

asterisk -rx "dialplan reload"

Manual IVR Testing

From the Asterisk CLI, originate a test call:

asterisk -rx "channel originate SIP/test-agent Extension 1000@from-vicidial-ivr"

Test IVR audio playback:

asterisk -rx "console dial 1000@from-vicidial-ivr"

Accessing ViciDial Logs

View call logs in ViciDial admin:

Query call logs directly:

SELECT * FROM vicidial_log WHERE called_number = '1234567890' 
AND call_date >= NOW() - INTERVAL 1 HOUR 
ORDER BY call_date DESC LIMIT 20;

Check agent state during IVR interaction:

SELECT user, status, phone_number, call_date FROM vicidial_agent_log 
WHERE call_date >= NOW() - INTERVAL 1 HOUR 
ORDER BY call_date DESC;

Summary

You now have a comprehensive production-grade ViciDial IVR setup with:

For production deployment:

  1. Backup your Asterisk configuration before making changes
  2. Test all dialplans with test calls before going live
  3. Monitor logs during the first 24 hours after deployment
  4. Set appropriate queue timeouts based on your staffing
  5. Record audio prompts professionally to improve caller experience
  6. Document custom contexts for future maintenance
  7. Implement call recording for compliance and training
  8. Schedule regular database maintenance on your custom IVR tables

The IVR system scales with ViciDial's queue management, allowing hundreds of simultaneous calls with intelligent routing based on business rules, not just static menus.

Need expert help with your setup?

VoIP infrastructure consulting, AI voice agent integration, monitoring stacks, scaling — I've done it all in production.

Get a Free Consultation