← All Tutorials

ViciDial Lead Recycling & List Management Best Practices

ViciDial Administration Intermediate 15 min read #47

Master advanced lead recycling strategies, implement intelligent list management workflows, and optimize your ViciDial contact center performance with production-tested configurations and real-world examples.

Prerequisites

Before implementing the strategies in this tutorial, ensure you have:

Verify your installation:

mysql -u root -p asterisk -e "SELECT VERSION();"
asterisk -v
grep "vicidial" /etc/asterisk/extensions-vicidial.conf | head -5

Understanding Lead Lifecycle in ViciDial

Lead recycling is the process of requeuing contacts after specific outcomes, allowing multiple attempts with different agents, strategies, or time intervals. Understanding the lead lifecycle is foundational to effective recycling.

Lead Status Codes and Their Meanings

ViciDial uses standardized status codes stored in the vicidial_log table. Key statuses relevant to recycling:

Status Code Meaning Recyclable Default Seconds
NEW Untouched lead Yes 0
CALL Active call No N/A
CALLACK Callback scheduled No User-defined
BUSY Busy signal Yes 3600
NOANSWER No answer Yes 1800
NOTCALLABLE Invalid number No 0
BADNUMBER Bad format No 0
DNC Do Not Call No 0
XFER Transfer attempted Yes 900
SALE Successful No 0
DEAD Duplicate/Dead No 0

The Recycling Decision Tree

Lead Called
    ├─ Answer
    │   ├─ Sale → No recycle
    │   ├─ Not Interested → Recycle (optional)
    │   └─ Wrong Number → No recycle (flag DNC)
    ├─ No Answer
    │   ├─ Retry ≤ 3 → Recycle NOW
    │   ├─ Retry 4-5 → Recycle in 24h
    │   └─ Retry > 5 → Archive
    └─ Busy
        ├─ Retry ≤ 2 → Recycle in 30 min
        └─ Retry > 2 → Recycle in 2h

Database Schema for Recycling Management

Key Tables

Understanding the database structure is essential for custom recycling logic.

-- View current lead status and recycle information
SELECT 
    vl.lead_id,
    vl.phone_number,
    vl.status,
    vl.call_date,
    vl.length_in_sec,
    vl.user,
    vl.campaign_id,
    COUNT(*) as attempt_count
FROM vicidial_log vl
WHERE vl.campaign_id = 'TESTCAMP'
    AND vl.phone_number = '5551234567'
GROUP BY vl.lead_id
ORDER BY vl.call_date DESC;

Core Recycling Tables

vicidial_list — Contains lead master records:

DESC vicidial_list;

Key fields for recycling:

vicidial_log — Call attempt history:

DESC vicidial_log;

Key fields:

vicidial_campaign — Campaign configuration:

DESC vicidial_campaign;

Critical recycling fields:

Configuring Lead Recycling Strategies

Method 1: Campaign-Level Automatic Recycling

Access the campaign configuration and enable recycling globally:

# Log into ViciDial web interface
# Navigate to: Admin → Campaigns → [Your Campaign]
# Look for these settings:

In the database, verify campaign recycling configuration:

SELECT 
    campaign_id,
    lead_recycle_method,
    auto_dial_level,
     voicemail_ext,
    hopper_level
FROM vicidial_campaign
WHERE campaign_id = 'TESTCAMP';

Update recycling method:

UPDATE vicidial_campaign
SET lead_recycle_method = 1
WHERE campaign_id = 'TESTCAMP';

Recycling methods:

Method 2: Status-Based Recycling Rules

Create granular recycling behavior through status configuration in vicidial_closer_log:

-- View current status recycling rules
SELECT 
    campaign_id,
    status,
    recyclable,
    rank
FROM vicidial_closer_log
WHERE campaign_id = 'TESTCAMP'
ORDER BY status;

This table determines which statuses trigger recycling. Insert recycling rules:

-- Recycle NOANSWER leads after 30 minutes
INSERT INTO vicidial_closer_log 
(campaign_id, status, recyclable, seconds_to_next_call, rank)
VALUES 
('TESTCAMP', 'NOANSWER', 'Y', 1800, 10)
ON DUPLICATE KEY UPDATE 
    recyclable = 'Y',
    seconds_to_next_call = 1800;

-- Recycle BUSY after 15 minutes
INSERT INTO vicidial_closer_log 
(campaign_id, status, recyclable, seconds_to_next_call, rank)
VALUES 
('TESTCAMP', 'BUSY', 'Y', 900, 20)
ON DUPLICATE KEY UPDATE 
    recyclable = 'Y',
    seconds_to_next_call = 900;

-- Do NOT recycle DNCPROSPECT
INSERT INTO vicidial_closer_log 
(campaign_id, status, recyclable, seconds_to_next_call, rank)
VALUES 
('TESTCAMP', 'DNCPROSPECT', 'N', 0, 5)
ON DUPLICATE KEY UPDATE 
    recyclable = 'N',
    seconds_to_next_call = 0;

Method 3: Custom Recycling with AGI Scripts

For advanced recycling logic, implement AGI (Asterisk Gateway Interface) scripts. Create a custom recycling script:

cat > /usr/share/astguiclient/agi-bin/recycle_custom.agi << 'EOF'
#!/usr/bin/perl
# Custom Lead Recycling AGI Script for ViciDial
# Place in: /usr/share/astguiclient/agi-bin/

use strict;
use warnings;
use Asterisk::AGI;
use DBI;

my $agi = new Asterisk::AGI;
my $lead_id = $agi->get_variable("lead_id");
my $campaign_id = $agi->get_variable("campaign_id");
my $status = $agi->get_variable("call_status");
my $attempt_count = $agi->get_variable("attempt_count");

# Connect to database
my $dbh = DBI->connect(
    "DBI:mysql:asterisk:localhost",
    "asteriskuser",
    "DBPASSWORD",
    {AutoCommit => 1}
) or die "Cannot connect: $DBI::errstr";

# Recycling logic
my $recycle = 0;
my $recycle_seconds = 0;

if ($status eq 'NOANSWER') {
    if ($attempt_count < 3) {
        $recycle = 1;
        $recycle_seconds = 1800;  # 30 minutes
    } elsif ($attempt_count < 5) {
        $recycle = 1;
        $recycle_seconds = 86400; # 24 hours
    }
} elsif ($status eq 'BUSY') {
    if ($attempt_count < 4) {
        $recycle = 1;
        $recycle_seconds = 900;   # 15 minutes
    }
} elsif ($status eq 'XFER' && $attempt_count < 2) {
    $recycle = 1;
    $recycle_seconds = 3600;     # 1 hour
}

# Update vicidial_list with recycle status
if ($recycle) {
    my $sth = $dbh->prepare(
        "UPDATE vicidial_list 
         SET status = ?, 
             last_local_call_time = DATE_ADD(NOW(), INTERVAL ? SECOND)
         WHERE lead_id = ?"
    );
    $sth->execute('RECYCLE', $recycle_seconds, $lead_id);
    $agi->set_variable("recycled", 1);
}

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

chmod +x /usr/share/astguiclient/agi-bin/recycle_custom.agi

Call this AGI from your dialplan:

; Add to /etc/asterisk/extensions-vicidial.conf
[vicidial-handler]
exten => s,1,AGI(agi-bin/recycle_custom.agi)
exten => s,n,Hangup()

Implementing Intelligent List Management

List Segmentation Strategy

Separate leads into logical groups for targeted recycling:

-- Create a custom list segmentation view
CREATE OR REPLACE VIEW lead_segments AS
SELECT 
    vl.lead_id,
    vl.phone_number,
    vl.list_id,
    vl.campaign_id,
    CASE 
        WHEN COUNT(*) <= 1 THEN 'FRESH'
        WHEN COUNT(*) BETWEEN 2 AND 3 THEN 'WARM'
        WHEN COUNT(*) > 3 THEN 'HOT'
    END as segment,
    COUNT(*) as attempt_count,
    MAX(vlog.call_date) as last_attempt
FROM vicidial_list vl
LEFT JOIN vicidial_log vlog ON vl.lead_id = vlog.lead_id
GROUP BY vl.lead_id;

-- Query segmented leads
SELECT * FROM lead_segments WHERE campaign_id = 'TESTCAMP' AND segment = 'FRESH';

Automatic List Purging and Archive

Prevent list bloat by archiving old, unrecyclable leads:

-- Archive DEAD leads older than 60 days
CREATE PROCEDURE archive_old_leads()
BEGIN
    INSERT INTO vicidial_list_archive
    SELECT * FROM vicidial_list
    WHERE status IN ('DEAD', 'NOTCALLABLE', 'BADNUMBER')
        AND DATE_SUB(NOW(), INTERVAL 60 DAY) > created_date;
    
    DELETE FROM vicidial_list
    WHERE status IN ('DEAD', 'NOTCALLABLE', 'BADNUMBER')
        AND DATE_SUB(NOW(), INTERVAL 60 DAY) > created_date;
END;

-- Execute once daily via cron
CALL archive_old_leads();

Add to crontab:

# Run daily at 2 AM
0 2 * * * mysql -u root -p'PASSWORD' asterisk -e "CALL archive_old_leads;"

Dynamic Callback Scheduling

Implement time-zone aware callbacks for optimal contact:

-- Schedule callbacks during business hours (9 AM - 6 PM local time)
UPDATE vicidial_list vl
SET status = 'CALLBACK',
    last_local_call_time = DATE_ADD(
        NOW(),
        INTERVAL (
            (9 - HOUR(CONVERT_TZ(NOW(), 'UTC', 'America/Chicago')))
            + IF(HOUR(CONVERT_TZ(NOW(), 'UTC', 'America/Chicago')) >= 18, 24, 0)
        ) HOUR
    )
WHERE lead_id IN (
    SELECT lead_id FROM vicidial_log 
    WHERE status = 'CALLBACK' 
        AND DATE_SUB(NOW(), INTERVAL 24 HOUR) < call_date
)
AND campaign_id = 'TESTCAMP';

Monitoring and Reporting Lead Recycling

Real-Time Recycling Dashboard Query

Monitor active recycling in real-time:

SELECT 
    COUNT(DISTINCT lead_id) as recyclable_leads,
    SUM(CASE WHEN status = 'RECYCLE' THEN 1 ELSE 0 END) as pending_recycle,
    SUM(CASE WHEN status = 'CALLBACK' THEN 1 ELSE 0 END) as scheduled_callbacks,
    campaign_id,
    MAX(last_local_call_time) as next_available
FROM vicidial_list
WHERE campaign_id = 'TESTCAMP'
    AND status IN ('RECYCLE', 'CALLBACK', 'BUSY', 'NOANSWER')
GROUP BY campaign_id;

Recycling Effectiveness Metrics

Measure recycling performance:

-- Calculate recycle-to-sale conversion rate
SELECT 
    vlog.campaign_id,
    COUNT(DISTINCT vlog.lead_id) as total_attempts,
    COUNT(DISTINCT CASE WHEN vlog.status = 'SALE' THEN vlog.lead_id END) as sales,
    ROUND(
        (COUNT(DISTINCT CASE WHEN vlog.status = 'SALE' THEN vlog.lead_id END) 
        / COUNT(DISTINCT vlog.lead_id) * 100), 2
    ) as conversion_rate_pct,
    AVG(CASE WHEN vlog.status = 'SALE' THEN vlog.length_in_sec ELSE NULL END) as avg_sale_duration
FROM vicidial_log vlog
WHERE vlog.campaign_id = 'TESTCAMP'
    AND vlog.call_date >= DATE_SUB(NOW(), INTERVAL 7 DAY)
GROUP BY vlog.campaign_id;

Lead Aging Report

Identify leads requiring urgency or removal:

-- List leads by age and status
SELECT 
    vl.lead_id,
    vl.phone_number,
    vl.status,
    DATEDIFF(NOW(), vl.created_date) as days_in_system,
    COUNT(vlog.lead_id) as total_attempts,
    MAX(vlog.call_date) as last_attempt,
    DATEDIFF(NOW(), MAX(vlog.call_date)) as days_since_last_attempt
FROM vicidial_list vl
LEFT JOIN vicidial_log vlog ON vl.lead_id = vlog.lead_id
WHERE vl.campaign_id = 'TESTCAMP'
GROUP BY vl.lead_id
HAVING days_in_system > 90
ORDER BY last_attempt DESC;

Configuring Recycling via CLI Commands

Bulk Recycle Operations

Recycle entire batches of leads using command-line operations:

# Recycle all NOANSWER leads from a specific campaign
mysql -u asteriskuser -p'PASSWORD' asterisk << EOF
UPDATE vicidial_list
SET status = 'RECYCLE',
    last_local_call_time = DATE_ADD(NOW(), INTERVAL 1800 SECOND)
WHERE campaign_id = 'TESTCAMP'
    AND status = 'NOANSWER'
    AND DATE_SUB(NOW(), INTERVAL 6 HOUR) > last_local_call_time;
EOF

Check Recycling Queue Status

Verify pending recycled leads:

# Connect to Asterisk CLI and check lead queue
asterisk -rx "agi show" | grep -i recycle

# Monitor ViciDial dialer process
ps aux | grep -E "vici_|astguiclient"

# Check for recycling script errors
tail -50 /var/log/asterisk/messages | grep -i recycle

Force Immediate Recycle

Manually trigger recycle for specific leads:

# SSH to ViciDial server
# Run manual recycle operation
mysql -u asteriskuser -p'PASSWORD' asterisk -e \
"UPDATE vicidial_list 
 SET status = 'RECYCLE', 
     last_local_call_time = NOW() 
 WHERE lead_id = 12345 
 AND campaign_id = 'TESTCAMP';"

# Verify update
mysql -u asteriskuser -p'PASSWORD' asterisk -e \
"SELECT lead_id, phone_number, status, last_local_call_time 
 FROM vicidial_list 
 WHERE lead_id = 12345;"

Advanced: Time-Zone Aware Recycling

Handle multi-zone campaigns properly:

-- Create time-zone aware recycling procedure
DELIMITER $$

CREATE PROCEDURE recycle_by_timezone(
    IN p_campaign_id VARCHAR(20)
)
BEGIN
    DECLARE done INT DEFAULT FALSE;
    DECLARE v_lead_id INT;
    DECLARE v_phone VARCHAR(20);
    DECLARE v_gmt_offset INT;
    DECLARE v_local_hour INT;
    
    DECLARE cur CURSOR FOR
        SELECT lead_id, phone_number, gmt_offset_now
        FROM vicidial_list
        WHERE campaign_id = p_campaign_id
            AND status IN ('NOANSWER', 'BUSY');
    
    DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE;
    
    OPEN cur;
    
    read_loop: LOOP
        FETCH cur INTO v_lead_id, v_phone, v_gmt_offset;
        IF done THEN
            LEAVE read_loop;
        END IF;
        
        -- Calculate local hour for this lead's timezone
        SET v_local_hour = HOUR(DATE_ADD(NOW(), INTERVAL v_gmt_offset HOUR));
        
        -- Only recycle during business hours (9 AM - 6 PM)
        IF v_local_hour >= 9 AND v_local_hour < 18 THEN
            UPDATE vicidial_list
            SET status = 'RECYCLE',
                last_local_call_time = NOW()
            WHERE lead_id = v_lead_id;
        END IF;
    END LOOP;
    
    CLOSE cur;
END$$

DELIMITER ;

-- Execute
CALL recycle_by_timezone('TESTCAMP');

Best Practices and Optimization

Lead Fatigue Prevention

Avoid over-calling contacts:

-- Set maximum attempts per lead
ALTER TABLE vicidial_list ADD COLUMN max_attempts INT DEFAULT 5;

-- Prevent recycling beyond maximum
UPDATE vicidial_list
SET status = 'MAXED'
WHERE (SELECT COUNT(*) FROM vicidial_log 
       WHERE lead_id = vicidial_list.lead_id) >= max_attempts
AND campaign_id = 'TESTCAMP';

Quality Assurance

Monitor recycling quality metrics:

#!/bin/bash
# Script: check_recycle_health.sh

CAMPAIGN="TESTCAMP"
THRESHOLD=10  # Alert if recycle queue exceeds 10%

TOTAL_LEADS=$(mysql -N -u asteriskuser -p'PASSWORD' asterisk \
    -e "SELECT COUNT(*) FROM vicidial_list WHERE campaign_id='$CAMPAIGN';")

RECYCLE_LEADS=$(mysql -N -u asteriskuser -p'PASSWORD' asterisk \
    -e "SELECT COUNT(*) FROM vicidial_list WHERE campaign_id='$CAMPAIGN' AND status='RECYCLE';")

PERCENTAGE=$((RECYCLE_LEADS * 100 / TOTAL_LEADS))

echo "Campaign: $CAMPAIGN"
echo "Total Leads: $TOTAL_LEADS"
echo "Recycled Leads: $RECYCLE_LEADS"
echo "Recycle Queue: $PERCENTAGE%"

if [ $PERCENTAGE -gt $THRESHOLD ]; then
    echo "WARNING: Recycle queue exceeds $THRESHOLD% threshold!"
fi

Run via cron:

0 * * * * /usr/local/bin/check_recycle_health.sh | mail -s "Recycle Health Alert" [email protected]

Database Maintenance

Regular optimization for recycling performance:

#!/bin/bash
# Add to crontab - Run weekly

mysql -u asteriskuser -p'PASSWORD' asterisk << EOF
-- Optimize tables for query performance
OPTIMIZE TABLE vicidial_list;
OPTIMIZE TABLE vicidial_log;
OPTIMIZE TABLE vicidial_closer_log;

-- Rebuild indexes
ANALYZE TABLE vicidial_list;
ANALYZE TABLE vicidial_log;

-- Check for corrupted records
CHECK TABLE vicidial_list;
CHECK TABLE vicidial_log;
EOF

Troubleshooting Lead Recycling Issues

Issue: Leads Not Recycling

Symptom: Status remains unchanged after expected recycle interval.

Diagnosis:

# 1. Check if campaign recycling is enabled
mysql -u asteriskuser -p'PASSWORD' asterisk -e \
"SELECT campaign_id, lead_recycle_method FROM vicidial_campaign WHERE campaign_id='TESTCAMP';"

# Expected output: lead_recycle_method should be 1 or higher

# 2. Verify vicidial_dialer process is running
ps aux | grep vicidial | grep -v grep

# 3. Check for errors in logs
tail -100 /var/log/asterisk/messages | grep -i recycle
grep "ERROR\|recycle\|RECYCLE" /var/log/asterisk/messages | tail -20

Solution:

# Enable recycling in campaign
mysql -u asteriskuser -p'PASSWORD' asterisk -e \
"UPDATE vicidial_campaign SET lead_recycle_method = 1 WHERE campaign_id = 'TESTCAMP';"

# Restart vicidial_dialer service
sudo service vicidial_dialer restart

# Monitor service startup
tail -f /var/log/asterisk/messages | grep vicidial

Issue: Recycled Leads Not Dialing

Symptom: RECYCLE status leads remain in queue but don't get dialed.

Diagnosis:

# Check hopper queue depth
mysql -u asteriskuser -p'PASSWORD' asterisk -e \
"SELECT COUNT(*) as hopper_count FROM vicidial_hopper WHERE campaign_id='TESTCAMP';"

# View next leads to be dialed
mysql -u asteriskuser -p'PASSWORD' asterisk -e \
"SELECT lead_id, phone_number, status, last_local_call_time 
 FROM vicidial_list 
 WHERE campaign_id = 'TESTCAMP' 
 AND status = 'RECYCLE' 
 ORDER BY last_local_call_time 
 LIMIT 10;"

# Check if leads meet recycle time requirement
mysql -u asteriskuser -p'PASSWORD' asterisk -e \
"SELECT lead_id, phone_number, status, 
 TIMESTAMPDIFF(SECOND, NOW(), last_local_call_time) as seconds_until_recycle
 FROM vicidial_list 
 WHERE campaign_id = 'TESTCAMP' 
 AND status = 'RECYCLE' 
 LIMIT 5;"

Solution:

# If leads show negative seconds_until_recycle, they're ready
# Check if hopper is full (capacity issue)

mysql -u asteriskuser -p'PASSWORD' asterisk -e \
"SELECT * FROM vicidial_campaign WHERE campaign_id = 'TESTCAMP';" | grep -E "hopper_level|lead_recycle"

# If hopper is too small, update it
mysql -u asteriskuser -p'PASSWORD' asterisk -e \
"UPDATE vicidial_campaign SET hopper_level = 200 WHERE campaign_id = 'TESTCAMP';"

# Force immediate hopper fill
asterisk -rx "vicidial hopper $CAMPAIGN"

Issue: Excessive Recycling / Lead Fatigue

Symptom: Leads dialed repeatedly in short intervals, agents complaining about repetition.

Diagnosis:

# Identify heavily recycled leads
mysql -u asteriskuser -p'PASSWORD' asterisk -e \
"SELECT 
    vl.lead_id, 
    vl.phone_number, 
    COUNT(*) as call_attempts,
    MIN(vlog.call_date) as first_call,
    MAX(vlog.call_date) as last_call,
    TIMESTAMPDIFF(DAY, MIN(vlog.call_date), MAX(vlog.call_date)) as days_span
 FROM vicidial_list vl
 INNER JOIN vicidial_log vlog ON vl.lead_id = vlog.lead_id
 WHERE vl.campaign_id = 'TESTCAMP'
 GROUP BY vl.lead_id
 HAVING call_attempts > 10
 ORDER BY call_attempts DESC
 LIMIT 20;"

Solution:

# Implement attempt limits in closer_log
mysql -u asteriskuser -p'PASSWORD' asterisk -e \
"INSERT INTO vicidial_closer_log 
 (campaign_id, status, recyclable, seconds_to_next_call, rank)
 VALUES ('TESTCAMP', 'NOANSWER', 'Y', 3600, 10)
 ON DUPLICATE KEY UPDATE seconds_to_next_call = 3600;"

# Archive over-attempted leads
mysql -u asteriskuser -p'PASSWORD' asterisk -e \
"UPDATE vicidial_list vl
 SET status = 'ARCHIVE'
 WHERE vl.campaign_id = 'TESTCAMP'
 AND (SELECT COUNT(*) FROM vicidial_log WHERE lead_id = vl.lead_id) > 8;"

Issue: Recycling Causing Database Lock

Symptom: Database queries slow, connection timeout errors, agents report lag.

Diagnosis:

# Check for long-running queries
mysql -u asteriskuser -p'PASSWORD' asterisk -e "SHOW PROCESSLIST;" | grep -i UPDATE

# Monitor table locks
mysqladmin -u asteriskuser -p'PASSWORD' -i 2 processlist | grep -E "UPDATE|LOCK"

# Check innodb status
mysql -u asteriskuser -p'PASSWORD' asterisk -e "SHOW ENGINE INNODB STATUS\G" | grep -A 20 "LATEST DETECTED DEADLOCK"

Solution:

# Implement batching in recycling procedures
mysql -u asteriskuser -p'PASSWORD' asterisk << EOF
-- Update in smaller chunks to reduce lock time
SET @batch_size = 100;
UPDATE vicidial_list vl
SET status = 'RECYCLE'
WHERE campaign_id = 'TESTCAMP'
    AND status = 'NOANSWER'
    AND lead_id IN (
        SELECT lead_id FROM (
            SELECT lead_id FROM vicidial_list vl2
            WHERE campaign_id = 'TESTCAMP' AND status = 'NOANSWER'
            LIMIT @batch_size
        ) tmp
    );
EOF

# Add index for faster recycling queries
mysql -u asteriskuser -p'PASSWORD' asterisk -e \
"ALTER TABLE vicidial_list ADD INDEX idx_recycle (campaign_id, status, last_local_call_time);"

Summary

ViciDial lead recycling is a sophisticated system requiring careful planning and ongoing optimization. The key takeaways:

  1. Understand Status Flow: Different statuses recycle at different intervals; configure per vicidial_closer_log.

  2. Database-Driven Configuration: Recycling behavior lives in vicidial_list, vicidial_log, and vicidial_campaign tables—customize via SQL.

  3. Time-Zone Awareness: Use gmt_offset_now and last_local_call_time to avoid off-hours contact.

  4. Prevent Lead Fatigue: Set attempt limits, monitor call frequency, and archive over-contacted leads.

  5. Monitor Performance: Track recycle-to-sale conversion rates, queue depths, and recycling effectiveness.

  6. Automate Maintenance: Use cron jobs and stored procedures to archive old leads, optimize tables, and maintain database health.

  7. Test Before Deploying: Always back up your database and test recycling changes in a staging environment first.

  8. Document Custom Logic: If implementing AGI scripts or complex procedures, maintain clear documentation for future maintenance.

Effective lead recycling balances outreach frequency with lead quality, agent productivity, and customer satisfaction. Use the SQL queries and scripts provided here as templates, adjusting parameters for your specific campaign requirements.

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