← All Tutorials

Asterisk Voicemail System — Setup, Email Integration & IVR

Infrastructure & DevOps Intermediate 13 min read #61

Learn to configure production-grade voicemail in Asterisk with email notifications, custom greetings, transcription, and IVR routing for ViciDial environments.

Prerequisites

Before starting this tutorial, ensure you have:

Verify Asterisk is running:

sudo asterisk -rx "core show version"
sudo systemctl status asterisk

Understanding Asterisk Voicemail Architecture

Asterisk voicemail operates through the app_voicemail module, which stores voicemail messages in the filesystem (typically /var/spool/asterisk/voicemail/) and maintains metadata in Asterisk's astdb or via ODBC connectivity.

The complete voicemail flow:

  1. Call arrives → matches voicemail extension in dialplan
  2. Answer and greeting → plays custom or default greeting
  3. Record message → stores to filesystem with timestamp
  4. Notification → triggers email dispatch via MTA
  5. Retrieval → agent retrieves via *98 or IVR menu
  6. Archive → system moves old messages or deletes per policy

In ViciDial environments, voicemail ties directly to the vicidial_users table and can be correlated with vicidial_log entries for compliance and reporting.

Section 1: Core Voicemail Configuration

Installing and Enabling app_voicemail

Verify app_voicemail is compiled:

sudo asterisk -rx "module show like voicemail"

If not loaded:

sudo asterisk -rx "module load app_voicemail"

To make it persistent across restarts, edit /etc/asterisk/modules.conf:

[modules]
autoload=yes

; Specific voicemail module
load => app_voicemail.so
load => res_adsi.so
load => res_smdi.so

Creating the Voicemail Context

Edit /etc/asterisk/voicemail.conf to define voicemail boxes and contexts:

[general]
; Format for storing voicemail
format=wav49|gsm|wav
; Maximum message length in seconds (600 = 10 minutes)
maxmessage=600
minmessage=3
; Silence threshold (in milliseconds)
silencethreshold=128
; Number of seconds of silence to end message
maxsilence=10
; Email notification settings
[email protected]
fromstring=ViciDial Voicemail
emaildateformat=%A, %B %d, %Y at %l:%M %p
attachvolumeenvelope=yes
attach=yes
; Envelope format and headers
emailsubject=You have a voicemail message from ${VM_CALLERID}
emailbody=Dear ${VM_NAME},\n\nYou have received a new voicemail message from ${VM_CALLERID}.\n\nMessage length: ${VM_DUR} seconds\nReceived: ${VM_DATE}\n\nPlease retrieve your message at your earliest convenience.\n\nViciDial Team

[vicidial_context]
; Mailbox format: mailbox=>password,fullname,email,pager_email,options
; Options include: u(unavail), b(busy), t(tempgreeting), g(greeting)
6001=>9999,Agent Smith,[email protected],,uj(voicemail-inactive)
6002=>8888,Agent Jones,[email protected],,uj(voicemail-inactive)
6003=>7777,Agent Brown,[email protected],,u
; Bulk add for 100 users (6100-6199)
6100=>1111,Sales Agent 100,[email protected]
6101=>1111,Sales Agent 101,[email protected]

The format breakdown:

Linking ViciDial Users to Voicemail

ViciDial's vicidial_users table has voicemail integration points. Query the schema:

SHOW COLUMNS FROM vicidial_users LIKE '%voicemail%';

If your ViciDial version supports it, you'll see fields like voicemail_ext or vm_enabled. Alternatively, create a mapping table:

CREATE TABLE IF NOT EXISTS vicidial_voicemail_map (
  user_id INT NOT NULL PRIMARY KEY,
  vm_ext VARCHAR(10),
  vm_password VARCHAR(10),
  vm_email VARCHAR(100),
  vm_enabled ENUM('Y','N') DEFAULT 'Y',
  INDEX idx_user_id (user_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

Populate it:

INSERT INTO vicidial_voicemail_map (user_id, vm_ext, vm_password, vm_email, vm_enabled)
SELECT user_id, extension, '9999', email, 'Y' 
FROM vicidial_users 
WHERE user_status='ACTIVE' AND role='AGENT';

Section 2: Dialplan Integration & IVR Routing

Creating the Voicemail Dialplan Context

Edit /etc/asterisk/extensions-vicidial.conf and add a dedicated voicemail context:

[voicemail-context]
; Main voicemail entry point
exten => s,1,Verbose(2,Entering voicemail context from ${CALLERID(num)})
exten => s,n,Set(VM_CONTEXT=vicidial_context)
exten => s,n,Playback(vm-welcome,noanswer)
exten => s,n,VoiceMail(${EXTEN}@${VM_CONTEXT},u)
exten => s,n,Hangup()

; Voicemail menu for agents (*98 access)
[voicemail-main-menu]
exten => s,1,NoOp(Voicemail Main Menu for ${CALLERID(num)})
exten => s,n,Set(ATTEMPT=0)
exten => s,n(authenticate)
exten => s,n,Set(ATTEMPT=$[${ATTEMPT} + 1])
exten => s,n,Playback(vm-login,noanswer)
exten => s,n,Read(VMBOX,vm-enter-id,5)
exten => s,n,Read(VMPASS,vm-enter-password,5)
exten => s,n,VoiceMailMain(${VMBOX}@${VM_CONTEXT})
exten => s,n,Hangup()

; Access voicemail directly with extension
exten => _610[0-9],1,NoOp(Direct voicemail for ${EXTEN})
exten => _610[0-9],n,VoiceMail(${EXTEN}@vicidial_context,u)
exten => _610[0-9],n,Hangup()

; Fallback for invalid extensions
exten => i,1,Playback(invalid,noanswer)
exten => i,n,Hangup()

IVR-Based Voicemail Routing

For more sophisticated call routing, create an IVR menu that directs calls to agent voicemail or live queue:

[vicidial-ivr-main]
exten => s,1,NoOp(=== ViciDial IVR Entry ===)
exten => s,n,Set(CHANNEL(language)=en)
exten => s,n,Playback(vm-goodbye,noanswer)
exten => s,n(menu)
exten => s,n,Playback(press-1-for-sales,noanswer)
exten => s,n,Playback(press-2-for-support,noanswer)
exten => s,n,Playback(press-0-operator,noanswer)
exten => s,n,WaitExten(5)

exten => 1,1,NoOp(=== Sales Queue ===)
exten => 1,n,Queue(sales_queue,t,,,300)
exten => 1,n,VoiceMail(6100@vicidial_context,u)
exten => 1,n,Hangup()

exten => 2,1,NoOp(=== Support Queue ===)
exten => 2,n,Queue(support_queue,t,,,300)
exten => 2,n,VoiceMail(6200@vicidial_context,u)
exten => 2,n,Hangup()

exten => 0,1,NoOp(=== Operator Transfer ===)
exten => 0,n,Dial(SIP/6001,20)
exten => 0,n,VoiceMail(6001@vicidial_context,b)
exten => 0,n,Hangup()

exten => t,1,Playback(vm-goodbye,noanswer)
exten => t,n,Hangup()

exten => i,1,Playback(invalid,noanswer)
exten => i,n,Goto(s,menu)

Assign this context to your inbound trunk in sip.conf or SIP configuration:

[my_carrier_trunk]
type=peer
context=vicidial-ivr-main
host=carrier.example.com
insecure=invite

Section 3: Email Integration & Notifications

Configuring Postfix for Voicemail Delivery

Ensure Postfix is installed and running:

sudo apt-get install postfix mailutils
sudo systemctl start postfix
sudo systemctl enable postfix

Test mail delivery:

echo "Test message" | mail -s "Test" [email protected]

Advanced Voicemail Email Configuration

Update /etc/asterisk/voicemail.conf with full email settings:

[general]
; Email delivery
[email protected]
fromstring=ViciDial Voicemail System
emaildateformat=%A, %B %d, %Y at %l:%M %p

; Attach voicemail as audio file
attach=yes
attachvolumeenvelope=yes

; Audio formats to attach (most compatible first)
format=wav49|gsm|wav

; Email body templates
emailsubject=Voicemail: ${VM_CALLERID} - ${VM_DATE}
emailbody=Dear ${VM_NAME},\n\nYou have a voicemail message.\n\nCaller: ${VM_CALLERID}\nLength: ${VM_DUR} seconds\nReceived: ${VM_DATE}\nBox: ${VM_MAILBOX}\n\nTo retrieve: Dial *98 and enter your voicemail password.\n\nViciDial System

; Pager/SMS format (text-only)
pagerfromstring=ViciDial Alerts
pagersubject=VM from ${VM_CALLERID}
pagerbody=${VM_NAME}, new voicemail from ${VM_CALLERID} (${VM_DUR}s) at ${VM_DATE}

; Maximum email size (prevent bloat)
volgain=0
emaildateformat=%A, %B %d, %Y at %l:%M %p

[vicidial_context]
; Per-mailbox override
6001=>9999,Agent Smith,[email protected],[email protected],uj
6002=>8888,Agent Jones,[email protected],,u

Custom Email Handler Script

For advanced processing (transcription, CRM integration), create /usr/local/bin/voicemail-handler.sh:

#!/bin/bash

# ViciDial Voicemail Handler Script
# Called by Asterisk after voicemail recording

VMBOX="$1"
CONTEXT="$2"
MAILBOX_DIR="/var/spool/asterisk/voicemail/${CONTEXT}/${VMBOX}"
CALLERID="$3"
DURATION="$4"

# Logging
LOG_FILE="/var/log/asterisk/voicemail-handler.log"
echo "$(date '+%Y-%m-%d %H:%M:%S') - Processing VM: Box=$VMBOX, Caller=$CALLERID, Duration=$DURATION" >> "$LOG_FILE"

# Get most recent message
LATEST_MSG=$(ls -t "$MAILBOX_DIR/new/"*.wav 2>/dev/null | head -1)

if [ -z "$LATEST_MSG" ]; then
    echo "$(date '+%Y-%m-%d %H:%M:%S') - ERROR: No message found for $VMBOX" >> "$LOG_FILE"
    exit 1
fi

# Convert to MP3 (optional, requires sox)
if command -v sox &> /dev/null; then
    MP3_FILE="${LATEST_MSG%.wav}.mp3"
    sox "$LATEST_MSG" -C 128 "$MP3_FILE" 2>> "$LOG_FILE"
    ATTACH_FILE="$MP3_FILE"
else
    ATTACH_FILE="$LATEST_MSG"
fi

# Query ViciDial database for mailbox owner info
AGENT_EMAIL=$(mysql -u asterisk -pasterisk asterisk -N -e \
    "SELECT email FROM vicidial_users WHERE extension='$VMBOX' LIMIT 1;" 2>/dev/null)

if [ -z "$AGENT_EMAIL" ]; then
    echo "$(date '+%Y-%m-%d %H:%M:%S') - WARNING: No email found for extension $VMBOX" >> "$LOG_FILE"
    exit 1
fi

# Send email with attachment
MAIL_SUBJECT="Voicemail from $CALLERID (${DURATION}s)"
MAIL_BODY="You have a new voicemail message.\n\nCaller: $CALLERID\nDuration: ${DURATION} seconds\nReceived: $(date '+%Y-%m-%d %H:%M:%S')\n\nPlease see attached audio file."

echo -e "$MAIL_BODY" | mutt -s "$MAIL_SUBJECT" -a "$ATTACH_FILE" -- "$AGENT_EMAIL" 2>> "$LOG_FILE"

echo "$(date '+%Y-%m-%d %H:%M:%S') - Email sent to $AGENT_EMAIL" >> "$LOG_FILE"

exit 0

Make it executable and update Asterisk to call it:

sudo chmod +x /usr/local/bin/voicemail-handler.sh

Register in voicemail.conf (requires custom module compilation for some versions):

[general]
; Call external script after voicemail saved
; Note: This requires res_voicemail compiled with SENDMAIL support
mailcmd=/usr/local/bin/voicemail-handler.sh

ODBC-Based Voicemail Storage (Alternative)

For high-volume environments, store voicemail metadata in MySQL instead of astdb:

Create the table:

CREATE TABLE asterisk.voicemail_msgs (
  id INT AUTO_INCREMENT PRIMARY KEY,
  context VARCHAR(50),
  mailbox VARCHAR(50),
  callerid VARCHAR(50),
  duration INT,
  message_date DATETIME,
  is_read ENUM('Y','N') DEFAULT 'N',
  INDEX idx_context_mailbox (context, mailbox),
  INDEX idx_date (message_date)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

Configure ODBC in /etc/asterisk/res_odbc.conf:

[asterisk]
dsn=asterisk
username=asterisk
password=asterisk
pooling=yes
limit=10

Update voicemail.conf to use ODBC:

[general]
; Enable ODBC support
odbcstorage=asterisk
odbctable=voicemail_msgs

Section 4: Agent Retrieval Interface

Enabling *98 Voicemail Access

Add to your main extension context in extensions-vicidial.conf:

[vicidial-agents]
; Agent voicemail retrieval (*98)
exten => 98,1,NoOp(Voicemail retrieval initiated by ${CALLERID(num)})
exten => 98,n,Authenticate(${EXTEN})
exten => 98,n,VoiceMailMain(@vicidial_context)
exten => 98,n,Hangup()

; Record temporary greeting (*99)
exten => 99,1,NoOp(Recording temporary greeting)
exten => 99,n,VoiceMailMain(${CALLERID(num)}@vicidial_context,t)
exten => 99,n,Hangup()

Include this context in your SIP extensions:

[6000-6299](vicidial_agent)
type=friend
context=vicidial-agents
disallow=all
allow=ulaw,gsm

Web-Based Voicemail Retrieval for ViciDial

Add a custom PHP interface at /var/www/html/vicidial/voicemail.php:

<?php
// ViciDial Web Voicemail Interface
session_start();

// Verify agent is logged in
if (!isset($_SESSION['user_id'])) {
    header("Location: index.php");
    exit;
}

$user_id = $_SESSION['user_id'];
$user_extension = $_SESSION['extension'];

// Database connection
$db = new mysqli("localhost", "asterisk", "asterisk", "asterisk");
if ($db->connect_error) {
    die("Database connection failed: " . $db->connect_error);
}

// Retrieve voicemail boxes for user
$query = "SELECT vm_ext, vm_email FROM vicidial_voicemail_map WHERE user_id = ?";
$stmt = $db->prepare($query);
$stmt->bind_param("i", $user_id);
$stmt->execute();
$result = $stmt->get_result();

?>
<!DOCTYPE html>
<html>
<head>
    <title>Voicemail Manager</title>
    <style>
        body { font-family: Arial, sans-serif; margin: 20px; }
        .vm-box { border: 1px solid #ccc; padding: 10px; margin: 10px 0; }
        .controls { margin: 10px 0; }
        button { padding: 8px 15px; cursor: pointer; }
    </style>
</head>
<body>
    <h2>Voicemail Manager</h2>
    
    <?php
    if ($result->num_rows > 0) {
        while ($row = $result->fetch_assoc()) {
            ?>
            <div class="vm-box">
                <h3>Extension <?php echo htmlspecialchars($row['vm_ext']); ?></h3>
                <p>Email: <?php echo htmlspecialchars($row['vm_email']); ?></p>
                <div class="controls">
                    <button onclick="accessVoicemail('<?php echo $row['vm_ext']; ?>')">
                        Access Voicemail
                    </button>
                    <button onclick="recordGreeting('<?php echo $row['vm_ext']; ?>')">
                        Record Greeting
                    </button>
                </div>
            </div>
            <?php
        }
    } else {
        echo "<p>No voicemail boxes configured for your account.</p>";
    }
    ?>
    
    <script>
    function accessVoicemail(ext) {
        // Trigger Asterisk originate call to VoiceMailMain
        fetch('voicemail-api.php', {
            method: 'POST',
            headers: {'Content-Type': 'application/x-www-form-urlencoded'},
            body: 'action=access&ext=' + encodeURIComponent(ext)
        })
        .then(response => response.json())
        .then(data => {
            if (data.success) {
                alert('Voicemail dialing to ' + ext);
            } else {
                alert('Error: ' + data.message);
            }
        });
    }
    
    function recordGreeting(ext) {
        if (confirm('Record temporary greeting for ' + ext + '?')) {
            fetch('voicemail-api.php', {
                method: 'POST',
                headers: {'Content-Type': 'application/x-www-form-urlencoded'},
                body: 'action=greeting&ext=' + encodeURIComponent(ext)
            })
            .then(response => response.json())
            .then(data => {
                alert(data.message);
            });
        }
    }
    </script>
</body>
</html>

Backend API at /var/www/html/vicidial/voicemail-api.php:

<?php
// Voicemail API for originating calls
session_start();

if (!isset($_SESSION['user_id'])) {
    http_response_code(401);
    echo json_encode(['success' => false, 'message' => 'Unauthorized']);
    exit;
}

$action = $_POST['action'] ?? '';
$ext = preg_replace('/[^0-9]/', '', $_POST['ext'] ?? '');

if (empty($ext)) {
    echo json_encode(['success' => false, 'message' => 'Invalid extension']);
    exit;
}

// Use Asterisk Manager Interface or AGI to originate call
$user_ext = $_SESSION['extension'];
$priority = ($action === 'greeting') ? 't' : 'u';

// Option 1: Use Asterisk Manager (requires AMI configuration)
$socket = fsockopen('localhost', 5038, $errno, $errstr, 2);
if (!$socket) {
    echo json_encode(['success' => false, 'message' => 'Cannot connect to Asterisk']);
    exit;
}

fwrite($socket, "Action: Login\r\nUsername: admin\r\nSecret: your_ami_secret\r\n\r\n");
fgets($socket, 1024);

$cmd = "Action: Originate\r\n"
     . "Channel: SIP/$user_ext\r\n"
     . "Context: vicidial-agents\r\n"
     . "Exten: $ext\r\n"
     . "Priority: 1\r\n"
     . "Async: true\r\n\r\n";

fwrite($socket, $cmd);
$response = fgets($socket, 1024);

fwrite($socket, "Action: Logoff\r\n\r\n");
fclose($socket);

echo json_encode(['success' => true, 'message' => 'Voicemail access initiated']);

Section 5: Advanced Features

Voicemail Transcription

Integrate with Google Cloud Speech-to-Text or Voicemail Transcription service:

#!/bin/bash
# /usr/local/bin/transcribe-voicemail.sh

VM_FILE="$1"
OUTPUT_FILE="${VM_FILE%.wav}.txt"

# Convert WAV to FLAC (Google requires)
flac "$VM_FILE" -o "${VM_FILE%.wav}.flac"

# Send to Google Speech-to-Text API
curl -X POST \
  --data-binary @"${VM_FILE%.wav}.flac" \
  -H "Content-Type: audio/flac" \
  "https://www.google.com/speech-api/json?output=json&client=chromium&key=YOUR_API_KEY" \
  > "$OUTPUT_FILE"

# Extract text from response
TRANSCRIPT=$(python3 -c "
import json
import sys
with open('$OUTPUT_FILE') as f:
    data = json.load(f)
    if 'result' in data and len(data['result']) > 0:
        print(data['result'][0]['alternative'][0]['transcript'])
" 2>/dev/null)

# Update database with transcript
mysql -u asterisk -pasterisk asterisk -e \
    "INSERT INTO voicemail_transcripts (vm_file, transcript, created_date) \
     VALUES ('$VM_FILE', '$TRANSCRIPT', NOW());"

echo "Transcription complete: $TRANSCRIPT"

Voicemail Purging & Archival

Create a maintenance script at /usr/local/bin/voicemail-maintenance.sh:

#!/bin/bash
# Archive and delete old voicemail messages

VOICEMAIL_ROOT="/var/spool/asterisk/voicemail"
ARCHIVE_DIR="/backup/voicemail-archive"
RETENTION_DAYS=90

# Create archive directory
mkdir -p "$ARCHIVE_DIR"

# Find and archive messages older than 90 days
find "$VOICEMAIL_ROOT" -type f -name "*.wav" -mtime +$RETENTION_DAYS | while read file; do
    dir=$(dirname "$file")
    context=$(basename $(dirname "$dir"))
    mailbox=$(basename "$dir")
    
    # Create archive structure
    archive_path="$ARCHIVE_DIR/$context/$mailbox"
    mkdir -p "$archive_path"
    
    # Move file to archive
    mv "$file" "$archive_path/"
    
    # Also move metadata
    base=$(basename "$file" .wav)
    [ -f "$dir/$base.txt" ] && mv "$dir/$base.txt" "$archive_path/"
    [ -f "$dir/$base.gsm" ] && mv "$dir/$base.gsm" "$archive_path/"
    
    echo "$(date) - Archived: $file"
done

# Tar and compress old archives (>30 days old)
find "$ARCHIVE_DIR" -type f -mtime +30 | xargs tar -czf "$ARCHIVE_DIR/voicemail-$(date +%Y%m%d).tar.gz"

echo "Voicemail maintenance completed at $(date)"

Schedule in crontab:

0 2 * * * /usr/local/bin/voicemail-maintenance.sh >> /var/log/voicemail-maintenance.log 2>&1

Voicemail Analytics & Reporting

Query voicemail activity:

-- Voicemail message count per agent
SELECT 
    vl.user_id,
    vu.user_name,
    vu.extension,
    COUNT(*) as vm_count,
    AVG(vl.message_length) as avg_duration,
    MAX(vl.call_date) as last_voicemail
FROM vicidial_log vl
JOIN vicidial_users vu ON vl.user_id = vu.user_id
WHERE vl.disposition = 'VOICEMAIL'
GROUP BY vl.user_id
ORDER BY vm_count DESC;

-- Voicemail response time (how long until agent retrieves)
SELECT 
    mailbox,
    COUNT(*) as total_voicemails,
    AVG(TIMESTAMPDIFF(HOUR, created, accessed)) as avg_retrieval_hours
FROM voicemail_msgs
WHERE accessed IS NOT NULL
GROUP BY mailbox;

Section 6: Troubleshooting

Voicemail Not Recording

Symptom: Calls go to voicemail but no message is left.

Diagnosis:

# Check voicemail module is loaded
sudo asterisk -rx "module show like voicemail"

# Check dialplan is correct
sudo asterisk -rx "dialplan show voicemail-context"

# Check filesystem permissions
ls -la /var/spool/asterisk/voicemail/
sudo chown -R asterisk:asterisk /var/spool/asterisk/voicemail/
sudo chmod -R 755 /var/spool/asterisk/voicemail/

Solution:

Reload dialplan and modules:

sudo asterisk -rx "dialplan reload"
sudo asterisk -rx "module reload app_voicemail"

Email Notifications Not Sending

Symptom: Voicemails recorded but no email received.

Diagnosis:

# Check mail service
sudo systemctl status postfix
sudo mailq

# Test mail delivery directly
echo "Test" | mail -s "Test Subject" [email protected]

# Check Asterisk logs
tail -f /var/log/asterisk/full
grep -i "voicemail\|email" /var/log/asterisk/full

# Verify voicemail.conf settings
grep -A5 "serveremail\|attach\|format" /etc/asterisk/voicemail.conf

Solution:

Ensure Postfix is running and configured:

sudo postfix start
# Test SMTP connectivity
telnet localhost 25

Verify voicemail.conf has attach=yes and correct serveremail.

Agent Cannot Access Voicemail via *98

Symptom: Dialing *98 hangs or says extension not found.

Diagnosis:

# Verify extension pattern
sudo asterisk -rx "dialplan show vicidial-agents"

# Check SIP registration
sudo asterisk -rx "sip show peers" | grep 600

# Test with asterisk -vvv
sudo asterisk -vvv
# Then dial *98 and watch console output

Solution:

Ensure voicemail-agents context is included in agent SIP profiles:

[6000-6299](vicidial_agent)
type=friend
context=vicidial-agents

Reload SIP:

sudo asterisk -rx "sip reload"

Voicemail Files Corrupted or Unplayable

Symptom: Voicemail records but plays as noise.

Diagnosis:

# Check file format and integrity
file /var/spool/asterisk/voicemail/vicidial_context/6001/INBOX/*.wav

# Play file manually
sox /var/spool/asterisk/voicemail/vicidial_context/6001/INBOX/msg0000.wav -t raw -

# Check codec mismatch in dialplan
sudo asterisk -rx "core show codecs"

Solution:

Ensure codec configuration matches in sip.conf:

allow=ulaw,gsm,g729
disallow=all

Verify voicemail format in voicemail.conf is compatible:

format=wav49|gsm|ulaw

Performance Issues with Large Voicemail Spool

Symptom: Asterisk slow, voicemail access lags.

Diagnosis:

# Check spool disk usage
du -sh /var/spool/asterisk/voicemail/

# Count total files
find /var/spool/asterisk/voicemail -type f | wc -l

# Check I/O wait
iostat -x 1 5

Solution:

Implement archival maintenance (see Section 5), migrate to ODBC backend, or use external storage:

# Enable S

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