The basic IVR setup with Asterisk and PJSIP covers menu prompts and simple call routing. That foundation works for small businesses, but production telephony demands more – call queues that distribute load across agents, dial-by-name directories, multi-language support, caller ID routing, callback handling, and programmable IVR logic through the Asterisk REST Interface (ARI). This guide covers all of those advanced features.
Every configuration in this article builds on a working Asterisk PBX installation with PJSIP and a functioning basic IVR. The examples target Asterisk 22 LTS running on RHEL 10, Rocky Linux 10, AlmaLinux 10, Debian 13, or Ubuntu 24.04. All dialplan snippets use the standard Asterisk dialplan syntax in extensions.conf unless noted otherwise. Refer to the official Asterisk documentation for version-specific details.
Prerequisites
- A working Asterisk PBX installation with PJSIP configured and tested
- A functioning basic IVR menu (Background() + WaitExten() or Read() based)
- Root or sudo access on the Asterisk server
- At least 2 SIP extensions registered for testing queue and transfer features
- Python 3.9+ or Node.js 20+ installed if you plan to use the ARI section
- Firewall ports: UDP 5060 (SIP), UDP 10000-20000 (RTP), TCP 8088 (ARI HTTP)
Step 1: Integrate Call Queues with Your Asterisk IVR
Call queues distribute incoming calls across a group of agents based on a strategy. When a caller presses an IVR option like “Press 1 for Sales”, the dialplan sends them into a queue where they hear music on hold and position announcements until an agent picks up.
Verify the Queue Module
The app_queue module ships with Asterisk by default. Confirm it is loaded.
$ sudo asterisk -rx "module show like app_queue"
Expected output:
Module Description Use Count Status
app_queue.so True Call Queueing 0 Running
1 modules loaded
If the module is not loaded, load it manually and add it to modules.conf.
$ sudo asterisk -rx "module load app_queue.so"
Configure queues.conf
Open the queue configuration file.
$ sudo vim /etc/asterisk/queues.conf
Add the general settings and queue definitions.
[general]
persistentmembers = yes ; Keep dynamic agents across restarts
autofill = yes ; Fill all waiting callers simultaneously
monitor-type = MixMonitor ; Use MixMonitor for call recording
[sales]
musicclass = default
strategy = rrmemory ; Round-robin with memory of last agent
timeout = 15 ; Ring each agent for 15 seconds
retry = 5 ; Wait 5 seconds before trying next agent
maxlen = 10 ; Maximum 10 callers in queue
wrapuptime = 10 ; 10 second cooldown after agent finishes a call
announce-frequency = 60 ; Announce position every 60 seconds
announce-holdtime = yes ; Tell callers estimated hold time
announce-position = yes ; Tell callers their position in queue
joinempty = yes ; Allow callers to join even if no agents
leavewhenempty = no ; Don't kick callers if agents leave
member => PJSIP/1001,0 ; Static agent - extension 1001, penalty 0
member => PJSIP/1002,0 ; Static agent - extension 1002, penalty 0
[support]
musicclass = default
strategy = fewestcalls ; Route to agent with fewest completed calls
timeout = 20
retry = 5
maxlen = 15
wrapuptime = 15
announce-frequency = 45
announce-holdtime = yes
announce-position = yes
joinempty = yes
leavewhenempty = no
member => PJSIP/1003,0
member => PJSIP/1004,0
The strategy parameter controls how calls are distributed. Available strategies:
| Strategy | Behavior |
|---|---|
| ringall | Ring all available agents simultaneously |
| leastrecent | Ring agent who has been idle the longest |
| fewestcalls | Ring agent who has answered the fewest calls |
| random | Ring a random available agent |
| rrmemory | Round-robin, remembering where it left off |
| linear | Ring agents in order listed, always starting from first |
| wrandom | Weighted random based on penalty values |
Route IVR Options to Queues
In your extensions.conf, connect IVR menu options to the queues.
$ sudo vim /etc/asterisk/extensions.conf
Add the queue routing inside your IVR context.
[moi-ivr]
exten => s,1,Answer()
same => n,Background(main-menu)
same => n,WaitExten(5)
exten => 1,1,Verbose(2,Caller selected Sales queue)
same => n,Playback(please-hold-while-we-connect-your-call)
same => n,Queue(sales,t,,,120) ; t = allow transfer, 120s max wait
same => n,Playback(vm-goodbye)
same => n,Hangup()
exten => 2,1,Verbose(2,Caller selected Support queue)
same => n,Playback(please-hold-while-we-connect-your-call)
same => n,Queue(support,t,,,180) ; 180s max wait for support
same => n,Playback(vm-goodbye)
same => n,Hangup()
Dynamic Agent Login and Logout
Static agents in queues.conf are always members. Dynamic agents can log in and out using feature codes. Add these to your default context in extensions.conf.
; Agent login - dial *45 to join the sales queue
exten => *45,1,Verbose(2,Agent ${CALLERID(num)} logging into sales queue)
same => n,AddQueueMember(sales,PJSIP/${CALLERID(num)})
same => n,Playback(agent-loginok)
same => n,Hangup()
; Agent logout - dial *46 to leave the sales queue
exten => *46,1,Verbose(2,Agent ${CALLERID(num)} logging out of sales queue)
same => n,RemoveQueueMember(sales,PJSIP/${CALLERID(num)})
same => n,Playback(agent-loggedoff)
same => n,Hangup()
Reload the configuration and verify.
$ sudo asterisk -rx "dialplan reload"
$ sudo asterisk -rx "queue reload all"
$ sudo asterisk -rx "queue show sales"
Expected output shows the queue members and their status:
sales has 0 calls (max 10) in 'rrmemory' strategy (0s holdtime, 0s talktime), W:0, C:0, A:0, SL:0.0%, SL2:0.0% within 0s
Members:
PJSIP/1001 (ringinuse disabled) (dynamic) (Not in use) has taken 0 calls (last was 0 secs ago)
PJSIP/1002 (ringinuse disabled) (dynamic) (Not in use) has taken 0 calls (last was 0 secs ago)
No Callers
Use queue show stats to see detailed call statistics for all queues.
Step 2: Set Up Dial-by-Name Directory
The Directory() application lets callers spell a person’s name on the phone keypad and get connected. This is useful when callers know who they want to reach but not the extension number.
Configure Voicemail Names
The directory uses names from voicemail.conf. Each mailbox needs a full name defined.
$ sudo vim /etc/asterisk/voicemail.conf
Add or update the mailbox entries with names.
[default]
1001 => 4321,John Smith,[email protected]
1002 => 4321,Jane Doe,[email protected]
1003 => 4321,Robert Johnson,[email protected]
1004 => 4321,Maria Garcia,[email protected]
Add Directory to the IVR Dialplan
Add an IVR option that launches the directory. The f flag searches by first name, l by last name, and fl by both.
; In your [moi-ivr] context
exten => 3,1,Verbose(2,Caller entering dial-by-name directory)
same => n,Directory(default,moi-ivr,f) ; Search by first name
same => n,Hangup()
; Alternative - search by last name
; exten => 3,1,Directory(default,moi-ivr,l)
; Alternative - let caller choose first or last name
; exten => 3,1,Directory(default,moi-ivr,fl)
The first argument (default) is the voicemail context to search. The second argument (moi-ivr) is the dialplan context where the matched extension will be dialed. Reload and test.
$ sudo asterisk -rx "dialplan reload"
$ sudo asterisk -rx "voicemail reload"
When a caller presses 3, they hear “Using the telephone keys, spell the first name of the person you wish to reach.” The caller enters letters using the phone keypad (e.g., 5-6-4-6 for “John”), and Asterisk presents matching entries.
Step 3: Build a Multi-Language IVR
Asterisk selects prompt files based on the CHANNEL(language) variable. By default, it uses en (English). Setting a different language code makes Asterisk look for prompts in the corresponding subdirectory under /var/lib/asterisk/sounds/.
Install Additional Sound Packages
Download Spanish (or other language) sound files from the Asterisk sound packages.
$ cd /var/lib/asterisk/sounds/
$ sudo wget https://downloads.asterisk.org/pub/telephony/sounds/asterisk-core-sounds-es-gsm-current.tar.gz
$ sudo tar xzf asterisk-core-sounds-es-gsm-current.tar.gz
$ sudo rm asterisk-core-sounds-es-gsm-current.tar.gz
Verify the directory structure.
$ ls /var/lib/asterisk/sounds/en/ | head -5
$ ls /var/lib/asterisk/sounds/es/ | head -5
Create the Language Selection Menu
Add a language selection context that runs before the main IVR menu.
[lang-select]
exten => s,1,Answer()
same => n,Set(CHANNEL(language)=en)
same => n,Background(press-1-for-english) ; Custom prompt
same => n,Background(es/oprima-dos-espanol) ; Custom Spanish prompt
same => n,WaitExten(5)
exten => 1,1,Set(CHANNEL(language)=en)
same => n,Goto(moi-ivr,s,1)
exten => 2,1,Set(CHANNEL(language)=es)
same => n,Goto(moi-ivr,s,1)
Record Custom Prompts per Language
Record custom prompts and save them in the correct language directory. Use the Record() application from a phone, or upload pre-recorded WAV files.
; Add a recording extension for admins
[recordings]
exten => *77,1,Playback(beep)
same => n,Record(/var/lib/asterisk/sounds/en/main-menu.gsm,3,30)
same => n,Playback(/var/lib/asterisk/sounds/en/main-menu)
same => n,Hangup()
exten => *78,1,Playback(beep)
same => n,Record(/var/lib/asterisk/sounds/es/main-menu.gsm,3,30)
same => n,Playback(/var/lib/asterisk/sounds/es/main-menu)
same => n,Hangup()
After recording, the main IVR automatically plays the correct language version because Background(main-menu) resolves to /var/lib/asterisk/sounds/en/main-menu.gsm or /var/lib/asterisk/sounds/es/main-menu.gsm based on the CHANNEL(language) value. Update your inbound route to point to lang-select instead of moi-ivr.
Step 4: Caller ID-Based Routing for VIP Callers
Route callers differently based on their Caller ID. VIP customers skip the queue, known callers get personalized greetings, and unknown numbers follow the standard IVR path.
Using AstDB for VIP Number Storage
Store VIP numbers in Asterisk’s built-in database (AstDB). This avoids external database dependencies for simple lookups.
$ sudo asterisk -rx "database put vip 15551234567 Gold"
$ sudo asterisk -rx "database put vip 15559876543 Platinum"
$ sudo asterisk -rx "database show vip"
Expected output:
/vip/15551234567 : Gold
/vip/15559876543 : Platinum
2 results found.
Dialplan for Caller ID Routing
Add VIP detection at the start of your IVR context, before the main menu plays.
[caller-routing]
exten => s,1,Answer()
same => n,Set(VIP_STATUS=${DB(vip/${CALLERID(num)})})
same => n,GotoIf($["${VIP_STATUS}" != ""]?vip-handler,s,1)
same => n,Goto(lang-select,s,1) ; Normal callers go to language selection
[vip-handler]
exten => s,1,Verbose(2,VIP caller detected: ${CALLERID(num)} - ${VIP_STATUS})
same => n,Playback(welcome-back)
same => n,GotoIf($["${VIP_STATUS}" = "Platinum"]?platinum)
same => n,Queue(sales,t,,,60) ; Gold VIPs - priority queue with shorter timeout
same => n,Hangup()
same => n(platinum),Dial(PJSIP/1001&PJSIP/1002,30) ; Platinum - direct ring all agents
same => n,Queue(sales,t,,,30) ; Fallback to queue if no answer
same => n,Hangup()
CRM Lookup with func_curl
For dynamic caller information, use func_curl to query an external CRM or API. First verify the module is loaded.
$ sudo asterisk -rx "module show like func_curl"
Add a CRM lookup to your routing context.
; Query CRM API for caller information
exten => s,1,Set(CRM_RESPONSE=${CURL(http://10.0.1.50:8080/api/caller/${CALLERID(num)})})
same => n,Set(CALLER_NAME=${CUT(CRM_RESPONSE,|,1)})
same => n,Set(CALLER_TIER=${CUT(CRM_RESPONSE,|,2)})
same => n,GotoIf($["${CALLER_TIER}" = "premium"]?vip-handler,s,1)
same => n,Goto(moi-ivr,s,1)
The API should return a pipe-delimited response like John Smith|premium. Keep the timeout short (default 3 seconds) to avoid blocking the dialplan if the API is unreachable.
Step 5: Implement Callback Requests
Allow callers to request a callback instead of waiting in a queue. The system collects their number, stores it, and initiates an outbound call when an agent is free.
Collect the Callback Number
Add a callback option to your IVR menu.
; In [moi-ivr] context
exten => 5,1,Verbose(2,Caller requesting callback)
same => n,Playback(please-enter-your-phone-number)
same => n,Read(CALLBACK_NUM,,10,3,5) ; Read 10 digits, 3 attempts, 5 sec timeout
same => n,SayDigits(${CALLBACK_NUM})
same => n,Playback(if-correct-press-1)
same => n,Read(CONFIRM,,1,1,3)
same => n,GotoIf($["${CONFIRM}" = "1"]?confirm)
same => n,Goto(moi-ivr,5,1) ; Start over if not confirmed
same => n(confirm),Set(DB(callbacks/${EPOCH}/${CALLBACK_NUM})=pending)
same => n,Playback(thank-you-for-calling)
same => n,Verbose(2,Callback stored for ${CALLBACK_NUM})
same => n,Hangup()
Automated Callback with Originate
Create a callback processing context that dials the caller and connects them to an agent.
[callback-out]
exten => s,1,Verbose(2,Initiating callback to ${CALLBACK_TARGET})
same => n,Set(CALLERID(name)=Company Callback)
same => n,Set(CALLERID(num)=15551112222)
same => n,Playback(hello-you-requested-callback)
same => n,Queue(sales,t,,,120)
same => n,Hangup()
Cron Job for Processing Callbacks
Create a script that reads pending callbacks from AstDB and triggers outbound calls through the Asterisk Manager Interface (AMI). Save this to /usr/local/bin/process_callbacks.sh.
#!/bin/bash
# /usr/local/bin/process_callbacks.sh
# Process pending callback requests from AstDB
CALLBACKS=$(asterisk -rx "database show callbacks" | grep "pending" | awk -F'/' '{print $3}')
for TIMESTAMP in $CALLBACKS; do
NUMBER=$(asterisk -rx "database show callbacks/$TIMESTAMP" | grep -oP '\d{10,}')
if [ -n "$NUMBER" ]; then
# Originate call via AMI using asterisk CLI
asterisk -rx "channel originate PJSIP/$NUMBER@trunk-out extension s@callback-out"
# Mark as processed
asterisk -rx "database put callbacks $TIMESTAMP processed"
logger "Callback initiated to $NUMBER"
# Wait 30 seconds between callbacks to avoid flooding
sleep 30
fi
done
Make it executable and add a cron entry.
$ sudo chmod +x /usr/local/bin/process_callbacks.sh
$ echo "*/5 * * * * root /usr/local/bin/process_callbacks.sh" | sudo tee /etc/cron.d/asterisk-callbacks
This runs every 5 minutes and processes any pending callbacks.
Step 6: ARI for Programmable IVR Logic
The Asterisk REST Interface (ARI) moves IVR logic out of the dialplan and into external applications written in Python, Node.js, or any language that can make HTTP requests and handle WebSocket events. Use ARI when your IVR needs complex branching, database queries, or integration with web services that would be painful in dialplan syntax.
Enable ARI in Asterisk
Configure the HTTP server first.
$ sudo vim /etc/asterisk/http.conf
Enable the HTTP server and set the bind address.
[general]
enabled = yes
bindaddr = 0.0.0.0
bindport = 8088
tlsenable = no ; Enable TLS in production with valid certificates
Now configure ARI credentials.
$ sudo vim /etc/asterisk/ari.conf
Add the ARI user configuration.
[general]
enabled = yes
pretty = yes
[ivr-app]
type = user
read_only = no
password = Str0ng_AR1_P@ss!
Reload the modules.
$ sudo asterisk -rx "module reload res_ari.so"
$ sudo asterisk -rx "module reload res_http_websocket.so"
Open the firewall port for ARI.
$ sudo firewall-cmd --permanent --add-port=8088/tcp
$ sudo firewall-cmd --reload
Test the ARI endpoint.
$ curl -s -u ivr-app:Str0ng_AR1_P@ss! http://127.0.0.1:8088/ari/asterisk/info | python3 -m json.tool | head -10
Route Calls to a Stasis Application
In the dialplan, hand control of the call to your ARI application using Stasis().
; In [moi-ivr] context
exten => 9,1,Verbose(2,Sending caller to ARI application)
same => n,Answer()
same => n,Stasis(my-ivr-app)
same => n,Hangup()
Python ARI Application Example
Install the ari-py library.
$ pip3 install ari requests websocket-client
Create the IVR application. Save this to /opt/asterisk-ivr/ari_ivr.py.
#!/usr/bin/env python3
"""ARI-based dynamic IVR application for Asterisk"""
import ari
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
client = ari.connect('http://127.0.0.1:8088', 'ivr-app', 'Str0ng_AR1_P@ss!')
def on_stasis_start(channel_obj, event):
"""Handle incoming call to the ARI application"""
channel = channel_obj['channel']
caller_id = event['channel']['caller']['number']
logger.info(f"New call from {caller_id}")
# Play welcome prompt
channel.play(media='sound:welcome')
# Play menu options
playback = channel.play(media='sound:press-1')
def on_dtmf(channel_obj, event):
digit = event['digit']
logger.info(f"DTMF received: {digit}")
if digit == '1':
# Query a database or API here
channel.play(media='sound:connecting')
# Create a new channel to bridge with
try:
outgoing = client.channels.originate(
endpoint='PJSIP/1001',
app='my-ivr-app',
appArgs='dialed'
)
bridge = client.bridges.create(type='mixing')
bridge.addChannel(channel=[channel.id, outgoing.id])
except Exception as e:
logger.error(f"Failed to connect: {e}")
channel.play(media='sound:an-error-has-occurred')
elif digit == '2':
channel.play(media='sound:vm-goodbye')
channel.hangup()
elif digit == '*':
# Return to main menu
channel.play(media='sound:press-1')
channel.on_event('ChannelDtmfReceived', on_dtmf)
client.on_channel_event('StasisStart', on_stasis_start)
client.run(apps='my-ivr-app')
Run the application.
$ python3 /opt/asterisk-ivr/ari_ivr.py
Node.js ARI Application Example
Install the ari-client library.
$ npm init -y
$ npm install ari-client
Create the application. Save this to /opt/asterisk-ivr/ari_ivr.js.
const ari = require('ari-client');
ari.connect('http://127.0.0.1:8088', 'ivr-app', 'Str0ng_AR1_P@ss!', (err, client) => {
if (err) { throw err; }
client.on('StasisStart', (event, channel) => {
const callerNum = event.channel.caller.number;
console.log(`New call from ${callerNum}`);
// Play menu
channel.play({ media: 'sound:press-1' });
channel.on('ChannelDtmfReceived', (event, channel) => {
const digit = event.digit;
console.log(`DTMF: ${digit}`);
if (digit === '1') {
// Connect to an agent
const dialed = client.Channel();
dialed.originate({
endpoint: 'PJSIP/1001',
app: 'my-ivr-app',
appArgs: 'dialed'
}, (err, dialed) => {
if (err) { throw err; }
const bridge = client.Bridge();
bridge.create({ type: 'mixing' }, (err, bridge) => {
bridge.addChannel({ channel: [channel.id, dialed.id] });
});
});
} else if (digit === '2') {
channel.play({ media: 'sound:vm-goodbye' });
setTimeout(() => { channel.hangup(); }, 2000);
}
});
});
client.start('my-ivr-app');
console.log('ARI IVR application started');
});
Run with node /opt/asterisk-ivr/ari_ivr.js. For production, use a process manager like systemd or PM2 to keep the application running.
When to Use ARI vs Dialplan
| Use Case | Dialplan | ARI |
|---|---|---|
| Simple IVR menus (press 1, 2, 3) | Best choice | Overkill |
| Static queue routing | Best choice | Overkill |
| Dynamic menu from database | Possible but messy | Best choice |
| CRM integration with complex logic | Limited | Best choice |
| Real-time dashboards | Not possible | Best choice |
| Speech recognition / NLP | Not possible | Best choice |
Step 7: CDR and IVR Statistics Tracking
Track which IVR options callers select and how they navigate through menus. This data drives optimization – if 80% of callers press 1, that option should stay first.
Log DTMF Choices with CDR Custom Fields
Use CDR(userfield) to store the caller’s menu selection in call detail records.
; At each IVR option, log the selection
exten => 1,1,Set(CDR(userfield)=IVR_SALES)
same => n,Verbose(2,IVR selection: Sales)
same => n,Queue(sales,t,,,120)
exten => 2,1,Set(CDR(userfield)=IVR_SUPPORT)
same => n,Verbose(2,IVR selection: Support)
same => n,Queue(support,t,,,180)
exten => 3,1,Set(CDR(userfield)=IVR_DIRECTORY)
same => n,Verbose(2,IVR selection: Directory)
same => n,Directory(default,moi-ivr,f)
Enable Channel Event Logging (CEL)
CEL provides more granular tracking than CDR. Configure it in cel.conf.
$ sudo vim /etc/asterisk/cel.conf
Enable CEL with the relevant event types.
[general]
enable = yes
apps = Queue,Dial,Directory,Stasis
events = ALL
Query IVR Statistics
If CDR data is stored in a database (MySQL/MariaDB), run queries to analyze IVR usage.
-- Most selected IVR options in the last 30 days
SELECT userfield AS ivr_option,
COUNT(*) AS total_calls,
ROUND(COUNT(*) * 100.0 / SUM(COUNT(*)) OVER(), 1) AS percentage
FROM cdr
WHERE calldate >= DATE_SUB(NOW(), INTERVAL 30 DAY)
AND userfield LIKE 'IVR_%'
GROUP BY userfield
ORDER BY total_calls DESC;
For a quick CLI report without database access, parse the CDR CSV file.
$ awk -F',' '{print $NF}' /var/log/asterisk/cdr-csv/Master.csv | grep '^IVR_' | sort | uniq -c | sort -rn
Step 8: Call Recording from IVR
Record calls after they leave the IVR and connect to an agent. MixMonitor() records both sides of the conversation into a single file. This is useful for quality assurance and compliance. For a deeper look at call transfer and forwarding features, see our separate guide.
Enable Recording in the Queue Context
Add a compliance announcement and start recording before connecting to the queue.
exten => 1,1,Set(CDR(userfield)=IVR_SALES)
same => n,Playback(this-call-may-be-recorded)
same => n,Set(MONITOR_FILENAME=/var/spool/asterisk/monitor/${STRFTIME(${EPOCH},,%Y%m%d-%H%M%S)}-${CALLERID(num)})
same => n,MixMonitor(${MONITOR_FILENAME}.wav,b)
same => n,Queue(sales,t,,,120)
same => n,StopMixMonitor()
same => n,Hangup()
The b option bridges both audio channels into one file. Recordings are saved with a timestamp and caller ID in the filename for easy searching.
Manage Recording Storage
Create the recording directory with proper permissions.
$ sudo mkdir -p /var/spool/asterisk/monitor
$ sudo chown asterisk:asterisk /var/spool/asterisk/monitor
Add a cleanup cron to remove recordings older than 90 days.
$ echo "0 2 * * * root find /var/spool/asterisk/monitor -name '*.wav' -mtime +90 -delete" | sudo tee /etc/cron.d/asterisk-recording-cleanup
Step 9: IVR Security and Abuse Prevention
An exposed IVR system is a target for toll fraud, brute force attacks, and resource exhaustion. Lock it down. For a full security hardening guide, see securing Asterisk and FreePBX from VoIP fraud.
Rate Limiting with GROUP() and GROUP_COUNT()
Limit concurrent calls per source IP or caller ID to prevent flooding.
[caller-routing]
exten => s,1,Set(GROUP(ivr)=${CALLERID(num)})
same => n,GotoIf($[${GROUP_COUNT(${CALLERID(num)}@ivr)} > 3]?flood)
same => n,Answer()
same => n,Goto(moi-ivr,s,1)
same => n(flood),Verbose(1,SECURITY: Rate limit exceeded for ${CALLERID(num)})
same => n,Playback(ss-noservice)
same => n,Hangup()
This limits each caller ID to a maximum of 3 concurrent calls through the IVR.
Block Known Spam Callers
Store blocked numbers in AstDB and check on each inbound call.
; Add blocked numbers
; asterisk -rx "database put blocked 15551234567 spam"
exten => s,1,Set(BLOCKED=${DB(blocked/${CALLERID(num)})})
same => n,GotoIf($["${BLOCKED}" != ""]?blocked)
same => n,Goto(caller-routing,s,1)
same => n(blocked),Verbose(1,BLOCKED caller: ${CALLERID(num)})
same => n,Hangup()
Set Maximum Call Duration
Prevent toll fraud by enforcing a maximum call duration on all calls through the IVR.
; Set 60-minute maximum call duration
exten => s,1,Set(TIMEOUT(absolute)=3600)
same => n,Set(TIMEOUT(digit)=5)
same => n,Set(TIMEOUT(response)=10)
Fail2ban Integration for SIP
Configure fail2ban to monitor Asterisk logs and block brute force attempts. Create the Asterisk jail configuration.
$ sudo vim /etc/fail2ban/jail.d/asterisk.conf
Add the following jail definition.
[asterisk]
enabled = true
filter = asterisk
action = iptables-allports[name=asterisk, protocol=all]
logpath = /var/log/asterisk/messages
maxretry = 3
bantime = 86400
findtime = 600
Restart fail2ban and verify the jail is active.
$ sudo systemctl restart fail2ban
$ sudo fail2ban-client status asterisk
Step 10: Performance Tuning for High-Volume IVR
When handling hundreds of concurrent IVR calls, default Asterisk settings are not enough. Tune the thread pool, optimize audio file access, and configure connection pooling.
Thread Pool Settings
Edit the Asterisk startup configuration.
$ sudo vim /etc/asterisk/asterisk.conf
Increase the thread pool for high call volumes.
[options]
maxcalls = 500 ; Maximum concurrent calls
maxload = 2.0 ; Stop accepting calls if load average exceeds this
[stasis]
threadpool_initial_size = 20 ; Starting threads for Stasis/ARI
threadpool_idle_timeout = 120 ; Seconds before idle threads exit
threadpool_max_size = 200 ; Maximum threads under load
Pre-cache Audio Prompts
Move frequently used prompts to a tmpfs mount to eliminate disk I/O latency.
$ echo "tmpfs /var/lib/asterisk/sounds/cache tmpfs size=100M,mode=0755 0 0" | sudo tee -a /etc/fstab
$ sudo mkdir -p /var/lib/asterisk/sounds/cache
$ sudo mount -a
$ sudo cp /var/lib/asterisk/sounds/en/main-menu.* /var/lib/asterisk/sounds/cache/
$ sudo cp /var/lib/asterisk/sounds/en/welcome.* /var/lib/asterisk/sounds/cache/
$ sudo chown -R asterisk:asterisk /var/lib/asterisk/sounds/cache
Reference cached prompts in the dialplan as cache/main-menu instead of main-menu.
Database Connection Pooling
If your IVR queries a database (for caller lookup, CDR, or dynamic menus), configure ODBC connection pooling in res_odbc.conf.
$ sudo vim /etc/asterisk/res_odbc.conf
Configure the pool with pre-connected connections.
[asterisk_db]
enabled => yes
dsn => asterisk-connector
username => asterisk
password => SecureDBPass123
pre-connect => yes
max_connections => 20 ; Maximum pooled connections
sanitysql => SELECT 1
idlecheck => 60 ; Check idle connections every 60 seconds
Reload the ODBC module after changes.
$ sudo asterisk -rx "module reload res_odbc.so"
$ sudo asterisk -rx "odbc show"
Complete Advanced IVR Dialplan Example
Here is a consolidated extensions.conf snippet that ties together all the features covered in this guide. This goes into your existing dialplan alongside your PJSIP endpoint configuration.
; ============================================
; Advanced IVR - extensions.conf
; ============================================
[globals]
MAX_CONCURRENT_IVR=3
MAX_CALL_DURATION=3600
; --- Inbound entry point ---
[inbound]
exten => s,1,NoOp(Incoming call from ${CALLERID(num)})
same => n,Set(GROUP(ivr)=${CALLERID(num)})
same => n,GotoIf($[${GROUP_COUNT(${CALLERID(num)}@ivr)} > ${MAX_CONCURRENT_IVR}]?flood)
same => n,Set(TIMEOUT(absolute)=${MAX_CALL_DURATION})
same => n,Set(BLOCKED=${DB(blocked/${CALLERID(num)})})
same => n,GotoIf($["${BLOCKED}" != ""]?blocked)
same => n,Set(VIP_STATUS=${DB(vip/${CALLERID(num)})})
same => n,GotoIf($["${VIP_STATUS}" != ""]?vip-handler,s,1)
same => n,Goto(lang-select,s,1)
same => n(flood),Playback(ss-noservice)
same => n,Hangup()
same => n(blocked),Hangup()
; --- Language selection ---
[lang-select]
exten => s,1,Answer()
same => n,Set(CHANNEL(language)=en)
same => n,Background(press-1-for-english)
same => n,WaitExten(5)
exten => 1,1,Set(CHANNEL(language)=en)
same => n,Goto(moi-ivr,s,1)
exten => 2,1,Set(CHANNEL(language)=es)
same => n,Goto(moi-ivr,s,1)
; --- Main IVR menu ---
[moi-ivr]
exten => s,1,Background(main-menu)
same => n,WaitExten(5)
exten => 1,1,Set(CDR(userfield)=IVR_SALES)
same => n,Playback(this-call-may-be-recorded)
same => n,MixMonitor(/var/spool/asterisk/monitor/${STRFTIME(${EPOCH},,%Y%m%d-%H%M%S)}-${CALLERID(num)}.wav,b)
same => n,Queue(sales,t,,,120)
same => n,Hangup()
exten => 2,1,Set(CDR(userfield)=IVR_SUPPORT)
same => n,Playback(this-call-may-be-recorded)
same => n,MixMonitor(/var/spool/asterisk/monitor/${STRFTIME(${EPOCH},,%Y%m%d-%H%M%S)}-${CALLERID(num)}.wav,b)
same => n,Queue(support,t,,,180)
same => n,Hangup()
exten => 3,1,Set(CDR(userfield)=IVR_DIRECTORY)
same => n,Directory(default,moi-ivr,f)
same => n,Hangup()
exten => 5,1,Set(CDR(userfield)=IVR_CALLBACK)
same => n,Read(CALLBACK_NUM,,10,3,5)
same => n,SayDigits(${CALLBACK_NUM})
same => n,Read(CONFIRM,,1,1,3)
same => n,GotoIf($["${CONFIRM}" = "1"]?confirm)
same => n,Goto(moi-ivr,5,1)
same => n(confirm),Set(DB(callbacks/${EPOCH}/${CALLBACK_NUM})=pending)
same => n,Playback(thank-you-for-calling)
same => n,Hangup()
exten => 9,1,Set(CDR(userfield)=IVR_ARI)
same => n,Stasis(my-ivr-app)
same => n,Hangup()
exten => i,1,Playback(invalid)
same => n,Goto(moi-ivr,s,1)
exten => t,1,Playback(vm-goodbye)
same => n,Hangup()
; --- VIP handler ---
[vip-handler]
exten => s,1,Playback(welcome-back)
same => n,MixMonitor(/var/spool/asterisk/monitor/${STRFTIME(${EPOCH},,%Y%m%d-%H%M%S)}-VIP-${CALLERID(num)}.wav,b)
same => n,GotoIf($["${VIP_STATUS}" = "Platinum"]?platinum)
same => n,Queue(sales,t,,,60)
same => n,Hangup()
same => n(platinum),Dial(PJSIP/1001&PJSIP/1002,30)
same => n,Queue(sales,t,,,30)
same => n,Hangup()
; --- Agent login/logout ---
[default]
exten => *45,1,AddQueueMember(sales,PJSIP/${CALLERID(num)})
same => n,Playback(agent-loginok)
same => n,Hangup()
exten => *46,1,RemoveQueueMember(sales,PJSIP/${CALLERID(num)})
same => n,Playback(agent-loggedoff)
same => n,Hangup()
; --- Callback processing ---
[callback-out]
exten => s,1,Set(CALLERID(name)=Company Callback)
same => n,Playback(hello-you-requested-callback)
same => n,Queue(sales,t,,,120)
same => n,Hangup()
Conclusion
This guide covered advanced Asterisk IVR features – call queues with multiple distribution strategies, dial-by-name directory, multi-language prompt switching, caller ID routing with VIP handling, callback requests, ARI-based programmable IVR in Python and Node.js, CDR tracking, call recording, and security hardening. Each feature builds on the basic IVR foundation and can be deployed independently or combined into a full production system.
For production deployment, enable TLS on the ARI interface, configure SRTP for media encryption, set up regular database backups for CDR and AstDB, and implement monitoring on Asterisk process health and queue wait times. Review the Asterisk ARI documentation for the full REST API reference when building custom applications.
Related Guides
- Configure IVR in Asterisk PBX with PJSIP on Linux
- Secure Asterisk and FreePBX from VoIP Fraud and Brute force attacks
- Asterisk Call Transfer and Forwarding: Easy Configuration Guide
- How To Install Asterisk 18 LTS on Debian 12/11/10
- Installing FreePBX 16 on Ubuntu 22.04/20.04/18.04/16.04





























































