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:

StrategyBehavior
ringallRing all available agents simultaneously
leastrecentRing agent who has been idle the longest
fewestcallsRing agent who has answered the fewest calls
randomRing a random available agent
rrmemoryRound-robin, remembering where it left off
linearRing agents in order listed, always starting from first
wrandomWeighted 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 CaseDialplanARI
Simple IVR menus (press 1, 2, 3)Best choiceOverkill
Static queue routingBest choiceOverkill
Dynamic menu from databasePossible but messyBest choice
CRM integration with complex logicLimitedBest choice
Real-time dashboardsNot possibleBest choice
Speech recognition / NLPNot possibleBest 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