Master ViciDial's REST API and webhook system to build click-to-call functionality and real-time CRM integrations for Asterisk-based contact centers
Prerequisites
Before starting, verify your environment meets these requirements:
- ViciDial version: 2.14+ (tested on 2.14.1 and 3.x)
- Asterisk version: 11+ (16+ recommended)
- MySQL/MariaDB: 5.7+ with vicidial database
- Linux OS: CentOS 7+, Debian 10+, or Ubuntu 18.04+
- Network access: Agent network to ViciDial server on ports 80, 443, 8080
- SSH access: Root or sudo privileges to ViciDial server
- PHP: 7.2+ (installed with ViciDial)
- curl, wget, or similar HTTP client installed on your system
- Firewall rules: Outbound HTTPS (443) for webhooks if posting to external services
- API credentials ready: ViciDial user account with API permissions enabled
Verify ViciDial installation:
mysql -u root -p asterisk -e "SHOW TABLES LIKE 'vicidial%';" | head -20
You should see at least vicidial_log, vicidial_user, and vicidial_list tables.
Understanding ViciDial API Architecture
ViciDial exposes two primary integration mechanisms:
1. REST API Endpoints (/agc/api.php)
- Programmatic call initiation
- Real-time call state querying
- Agent status management
- Campaign and list operations
- Response format: JSON
2. Webhook System
- Event-triggered HTTP POST callbacks
- Call disposition logging
- Call completion notifications
- Custom integration points
Both systems authenticate via API key or username/password, and both log activity to the ViciDial database.
Section 1: Setting Up API Access & Authentication
Creating an API User Account
Log into ViciDial admin panel and navigate to Admin > Users > Add User, or use the MySQL shell:
USE asterisk;
INSERT INTO vicidial_user (
user,
pass,
user_level,
active,
api_enabled,
api_key,
full_name,
email
) VALUES (
'api_user',
MD5('P@ssw0rd!Secure'),
'8',
'Y',
'Y',
MD5(CONCAT(NOW(), RAND())),
'API Integration User',
'[email protected]'
);
Retrieve your API key:
mysql -u root -p asterisk -N -e "SELECT api_key FROM vicidial_user WHERE user='api_user';"
Store this key securely. It's your authentication token for all API calls.
Verifying API Permissions
Ensure the user has sufficient privileges:
SELECT user, user_level, active, api_enabled FROM vicidial_user WHERE user='api_user';
User levels:
- 8–10: Full API access (required)
- 1–7: Limited or no API access
If api_enabled is not Y, update it:
UPDATE vicidial_user SET api_enabled='Y' WHERE user='api_user';
Restart the ViciDial daemon to apply changes:
sudo service asterisk restart
sudo service vicidial restart
Section 2: Click-to-Call Implementation
Core Click-to-Call Workflow
Click-to-call initiates a call from a web form or CRM button, triggering ViciDial to call an agent, then bridge the customer.
Flow:
- User/agent clicks button in CRM
- JavaScript sends HTTP request to ViciDial API
- API validates caller, creates call record
- Asterisk dials agent's phone
- Once answered, Asterisk dials customer
- Bridge established; call logged
API Endpoint: /agc/api.php
Method: GET or POST
Required parameters:
function=click2calluser: ViciDial username (agent)passORapi_key: Authenticationphone_number: Customer phone to dialcampaign_id(optional): Assign to campaignlist_id(optional): Customer list IDcaller_id_number(optional): Outbound CID
Example 1: Basic Click-to-Call via cURL
#!/bin/bash
VICIDIAL_URL="http://vicidial.example.com/agc/api.php"
API_USER="api_user"
API_KEY="your_api_key_here"
AGENT_USER="agent_001"
CUSTOMER_PHONE="18005551234"
CAMPAIGN_ID="1234567890"
curl -X GET "${VICIDIAL_URL}?function=click2call&user=${API_USER}&api_key=${API_KEY}&agent_user=${AGENT_USER}&phone_number=${CUSTOMER_PHONE}&campaign_id=${CAMPAIGN_ID}"
# Response: status=SUCCESS or error details
Full-featured example with error handling:
#!/bin/bash
VICIDIAL_HOST="vicidial.example.com"
VICIDIAL_PORT="80"
API_USER="api_user"
API_KEY="$(mysql -u root -p asterisk -N -e 'SELECT api_key FROM vicidial_user WHERE user=\"api_user\";')"
function click_to_call() {
local agent_user=$1
local phone_number=$2
local campaign_id=$3
local response=$(curl -s "http://${VICIDIAL_HOST}:${VICIDIAL_PORT}/agc/api.php" \
--data-urlencode "function=click2call" \
--data-urlencode "user=${API_USER}" \
--data-urlencode "api_key=${API_KEY}" \
--data-urlencode "agent_user=${agent_user}" \
--data-urlencode "phone_number=${phone_number}" \
--data-urlencode "campaign_id=${campaign_id}")
echo "$response"
if echo "$response" | grep -q "status=SUCCESS"; then
echo "✓ Click-to-call initiated for $phone_number"
return 0
else
echo "✗ Click-to-call failed: $response"
return 1
fi
}
# Usage
click_to_call "agent_001" "18005551234" "1234567890"
Example 2: Click-to-Call via PHP (CRM Integration)
Create /var/www/html/crm_click2call.php:
<?php
/**
* CRM Click-to-Call Integration
* POST endpoint to initiate calls from external CRM
*/
// Configuration
define('VICIDIAL_HOST', 'vicidial.example.com');
define('API_USER', 'api_user');
define('API_KEY', 'your_api_key_here');
// Security headers
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: https://crm.example.com');
header('Access-Control-Allow-Methods: POST, OPTIONS');
// Validate origin
$allowed_origins = ['https://crm.example.com', 'https://admin.example.com'];
if (!in_array($_SERVER['HTTP_ORIGIN'] ?? '', $allowed_origins)) {
http_response_code(403);
echo json_encode(['status' => 'FAILED', 'error' => 'Unauthorized origin']);
exit(1);
}
// Parse request
$input = json_decode(file_get_contents('php://input'), true);
if (!isset($input['agent_user'], $input['phone_number'])) {
http_response_code(400);
echo json_encode(['status' => 'FAILED', 'error' => 'Missing required fields']);
exit(1);
}
// Validate phone format
$phone = preg_replace('/[^0-9]/', '', $input['phone_number']);
if (strlen($phone) < 10 || strlen($phone) > 15) {
http_response_code(400);
echo json_encode(['status' => 'FAILED', 'error' => 'Invalid phone format']);
exit(1);
}
// Build API request
$params = [
'function' => 'click2call',
'user' => API_USER,
'api_key' => API_KEY,
'agent_user' => $input['agent_user'],
'phone_number' => $phone,
'campaign_id' => $input['campaign_id'] ?? '',
'list_id' => $input['list_id'] ?? '',
'caller_id_number' => $input['caller_id'] ?? '',
];
$url = 'http://' . VICIDIAL_HOST . '/agc/api.php?' . http_build_query($params);
// Execute request with timeout
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 5,
CURLOPT_FOLLOWLOCATION => false,
]);
$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
// Parse ViciDial response
if ($http_code === 200) {
parse_str($response, $result);
http_response_code(200);
echo json_encode([
'status' => $result['status'] ?? 'UNKNOWN',
'message' => $result['result'] ?? 'No response',
]);
} else {
http_response_code(502);
echo json_encode(['status' => 'FAILED', 'error' => 'ViciDial unreachable']);
}
?>
Call from JavaScript:
async function initiateClickToCall(agentUser, customerPhone) {
const response = await fetch('https://your-domain.com/crm_click2call.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({
agent_user: agentUser,
phone_number: customerPhone,
campaign_id: '1234567890'
})
});
const result = await response.json();
if (result.status === 'SUCCESS') {
console.log('✓ Call initiated');
} else {
console.error('✗ Call failed:', result.message);
}
}
// Usage
document.getElementById('callBtn').addEventListener('click', () => {
initiateClickToCall('agent_001', '18005551234');
});
Section 3: CRM Webhooks & Event Notifications
Configuring Outbound Webhooks
ViciDial can POST call events (disposition, completion, etc.) to external URLs. Configure via the database:
INSERT INTO vicidial_settings (
setting_name,
setting_value
) VALUES (
'WEBHOOK_ENABLED',
'Y'
),
(
'WEBHOOK_URL',
'https://crm.example.com/webhook/vicidial'
),
(
'WEBHOOK_EVENTS',
'call_complete,disposition_set'
);
Or configure in /etc/asterisk/extensions-vicidial.conf:
[from-internal-ivr]
exten => _X.,1,Log(NOTICE, ViciDial webhook event: ${EXTEN})
exten => _X.,n,System(curl -X POST https://crm.example.com/webhook/vicidial \
-d "event_type=call_complete&uniqueid=${UNIQUEID}&calldate=${CALLDATE}")
Webhook Payload Structure
ViciDial sends POST requests with call data:
{
"event": "call_complete",
"uniqueid": "1234567890.123",
"calldate": "2024-01-15 14:32:00",
"phone_number": "18005551234",
"agent_user": "agent_001",
"campaign_id": "1234567890",
"disposition": "SALE",
"call_duration": "245",
"recording_filename": "/var/spool/asterisk/monitor/20240115-143200-18005551234.wav",
"notes": "Customer interested in premium plan"
}
Webhook Receiver Implementation
Create /var/www/html/webhook_receiver.php:
<?php
/**
* ViciDial Webhook Receiver
* Logs and processes call events from ViciDial
*/
// Configuration
define('LOG_FILE', '/var/log/vicidial_webhook.log');
define('DB_HOST', 'localhost');
define('DB_USER', 'root');
define('DB_PASS', 'password');
define('DB_NAME', 'your_crm_db');
define('WEBHOOK_SECRET', 'shared_secret_key');
// Security: Validate request
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
exit('Method not allowed');
}
// Log raw request
$raw_input = file_get_contents('php://input');
file_put_contents(LOG_FILE, date('Y-m-d H:i:s') . " | Raw input: $raw_input\n", FILE_APPEND);
// Parse payload
$payload = json_decode($raw_input, true);
if (!$payload) {
$payload = [];
parse_str($raw_input, $payload);
}
// Validate required fields
$required_fields = ['event', 'uniqueid', 'phone_number', 'agent_user'];
foreach ($required_fields as $field) {
if (empty($payload[$field])) {
http_response_code(400);
file_put_contents(LOG_FILE, date('Y-m-d H:i:s') . " | Missing field: $field\n", FILE_APPEND);
exit(json_encode(['status' => 'error', 'message' => "Missing $field"]));
}
}
// Process event
$event = $payload['event'];
$uniqueid = $payload['uniqueid'];
$phone = $payload['phone_number'];
$agent = $payload['agent_user'];
$disposition = $payload['disposition'] ?? 'NO DISPOSITION';
$call_duration = $payload['call_duration'] ?? 0;
// Log to file
$log_message = sprintf(
"Event: %s | UniqueID: %s | Phone: %s | Agent: %s | Disposition: %s | Duration: %ss\n",
$event, $uniqueid, $phone, $agent, $disposition, $call_duration
);
file_put_contents(LOG_FILE, date('Y-m-d H:i:s') . " | " . $log_message, FILE_APPEND);
// Database integration
try {
$pdo = new PDO(
"mysql:host=" . DB_HOST . ";dbname=" . DB_NAME,
DB_USER,
DB_PASS,
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
);
// Example: Update CRM contact record
if ($event === 'call_complete') {
$stmt = $pdo->prepare("
UPDATE contacts
SET last_call_date = NOW(),
last_agent = ?,
last_disposition = ?,
last_call_duration = ?
WHERE phone_number = ?
");
$stmt->execute([$agent, $disposition, $call_duration, $phone]);
}
// Log to webhook_events table
$stmt = $pdo->prepare("
INSERT INTO webhook_events (event_type, uniqueid, phone, agent, disposition, duration, payload, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, NOW())
");
$stmt->execute([
$event,
$uniqueid,
$phone,
$agent,
$disposition,
$call_duration,
$raw_input
]);
} catch (PDOException $e) {
file_put_contents(LOG_FILE, date('Y-m-d H:i:s') . " | DB Error: " . $e->getMessage() . "\n", FILE_APPEND);
http_response_code(500);
exit(json_encode(['status' => 'error', 'message' => 'Database error']));
}
// Success response
http_response_code(200);
echo json_encode(['status' => 'success', 'uniqueid' => $uniqueid]);
?>
Create CRM Database Table
CREATE TABLE webhook_events (
id INT AUTO_INCREMENT PRIMARY KEY,
event_type VARCHAR(50) NOT NULL,
uniqueid VARCHAR(50) NOT NULL UNIQUE,
phone VARCHAR(15) NOT NULL,
agent VARCHAR(50),
disposition VARCHAR(100),
duration INT DEFAULT 0,
payload LONGTEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
processed TINYINT DEFAULT 0,
INDEX idx_phone (phone),
INDEX idx_agent (agent),
INDEX idx_created (created_at)
);
Section 4: Advanced API Queries
Query Agent Status in Real-Time
#!/bin/bash
# Get agent status
curl -s "http://vicidial.example.com/agc/api.php" \
--data-urlencode "function=get_agent_status" \
--data-urlencode "user=api_user" \
--data-urlencode "api_key=your_api_key" \
--data-urlencode "agent_user=agent_001" | head -20
Parse response in PHP:
$response = file_get_contents('http://vicidial.example.com/agc/api.php?function=get_agent_status&user=api_user&api_key=KEY&agent_user=agent_001');
parse_str($response, $status);
echo "Status: " . $status['status'] . "\n";
echo "Calls today: " . $status['calls_today'] . "\n";
echo "Talk time: " . $status['talk_time'] . "\n";
Batch List Import via API
#!/bin/bash
VICIDIAL_URL="http://vicidial.example.com/agc/api.php"
API_USER="api_user"
API_KEY="your_api_key"
LIST_ID="4567890123"
CAMPAIGN_ID="1234567890"
# CSV file format: phone_number,first_name,last_name,email
while IFS=',' read phone fname lname email; do
curl -s -X GET "$VICIDIAL_URL" \
--data-urlencode "function=add_lead" \
--data-urlencode "user=$API_USER" \
--data-urlencode "api_key=$API_KEY" \
--data-urlencode "list_id=$LIST_ID" \
--data-urlencode "phone_number=$phone" \
--data-urlencode "first_name=$fname" \
--data-urlencode "last_name=$lname" \
--data-urlencode "email=$email" \
--data-urlencode "campaign_id=$CAMPAIGN_ID"
echo "Added: $phone"
done < leads.csv
Get Call Recording Path
SELECT
uniqueid,
phone_number,
call_date,
recording_filename
FROM vicidial_log
WHERE phone_number='18005551234'
AND call_date >= DATE_SUB(NOW(), INTERVAL 7 DAY)
ORDER BY call_date DESC
LIMIT 5;
Output:
| uniqueid | phone_number | call_date | recording_filename |
|----------|--------------|---------------------|--------------------------------------------|
| 1234.5 | 18005551234 | 2024-01-15 14:32:00 | /var/spool/asterisk/monitor/20240115-143200-18005551234.wav |
Download recording:
#!/bin/bash
RECORDING_PATH="/var/spool/asterisk/monitor/20240115-143200-18005551234.wav"
DESTINATION="/backups/call_recordings/"
if [ -f "$RECORDING_PATH" ]; then
cp "$RECORDING_PATH" "$DESTINATION"
sox "$DESTINATION/$(basename $RECORDING_PATH)" -c 1 -r 8000 /tmp/converted.wav
echo "Recording archived and converted"
else
echo "Recording not found"
fi
Section 5: Asterisk Integration & Custom Dialplan
Trigger Webhooks from Dialplan
Add to /etc/asterisk/extensions-vicidial.conf:
[macro-post-call-webhook]
exten => s,1,NoOp(Posting call completion webhook)
exten => s,n,Set(WEBHOOK_URL=https://crm.example.com/webhook/vicidial)
exten => s,n,Set(WEBHOOK_PAYLOAD=event=call_complete&uniqueid=${UNIQUEID}&phone=${CALLERID(num)}&duration=${CDR(billsec)}&disposition=${DISPOSITION})
exten => s,n,System(curl -X POST "${WEBHOOK_URL}" \
-d "${WEBHOOK_PAYLOAD}" \
--connect-timeout 3 --max-time 5)
exten => s,n,Return()
[from-internal-vicidial]
exten => _[0-9*].,1,Gosub(macro-post-call-webhook,s,1)
exten => _[0-9*].,n,ViciDial(${EXTEN})
Store Call Data via Asterisk AGI
Create /var/lib/asterisk/agi-bin/vicidial_webhook.agi:
#!/usr/bin/perl
# AGI script to post call data to webhook
use strict;
use LWP::UserAgent;
use JSON;
my %AGI;
while (<STDIN>) {
chomp;
last if !$_;
if (/^agi_(\S+):\s(.*)/) { $AGI{$1} = $2; }
}
my $ua = LWP::UserAgent->new(timeout => 5);
my $webhook_url = 'https://crm.example.com/webhook/vicidial';
my $payload = {
event => 'call_complete',
uniqueid => $AGI{uniqueid},
phone_number => $AGI{callerid},
call_date => scalar localtime,
disposition => $AGI{context} || 'UNKNOWN',
};
my $response = $ua->post(
$webhook_url,
Content => to_json($payload),
'Content-Type' => 'application/json'
);
print STDERR "Webhook response: " . $response->status_line . "\n" if !$response->is_success;
Make executable:
chmod +x /var/lib/asterisk/agi-bin/vicidial_webhook.agi
Section 6: Logging, Monitoring & Debugging
Enable API Logging
Check ViciDial API logs:
tail -f /var/log/asterisk/messages | grep "api"
Or query database directly:
SELECT * FROM vicidial_log
WHERE uniqueid LIKE '%api%'
OR comments LIKE '%API%'
ORDER BY call_date DESC
LIMIT 20;
Test API Connectivity
#!/bin/bash
VICIDIAL_HOST="vicidial.example.com"
PORT="80"
echo "Testing ViciDial API connectivity..."
# Test HTTP connectivity
if curl -s -o /dev/null -w "%{http_code}" "http://${VICIDIAL_HOST}:${PORT}/agc/api.php" | grep -q "200\|400"; then
echo "✓ HTTP connectivity OK"
else
echo "✗ HTTP connectivity FAILED"
exit 1
fi
# Test API authentication
API_KEY="your_api_key"
RESPONSE=$(curl -s "http://${VICIDIAL_HOST}/agc/api.php?function=get_agent_status&user=api_user&api_key=${API_KEY}&agent_user=test")
if echo "$RESPONSE" | grep -q "status="; then
echo "✓ API authentication OK"
echo "Response: $RESPONSE"
else
echo "✗ API authentication FAILED"
exit 1
fi
Monitor Webhook Delivery
Create a monitoring script:
#!/bin/bash
LOG_FILE="/var/log/vicidial_webhook.log"
ERROR_COUNT=$(grep -c "DB Error\|FAILED" "$LOG_FILE" 2>/dev/null || echo 0)
SUCCESS_COUNT=$(grep -c "call_complete" "$LOG_FILE" 2>/dev/null || echo 0)
echo "Webhook Statistics (last hour):"
echo " Successful events: $SUCCESS_COUNT"
echo " Errors: $ERROR_COUNT"
if [ $ERROR_COUNT -gt 10 ]; then
echo "⚠ Warning: High error rate detected"
tail -20 "$LOG_FILE"
fi
Run periodically via cron:
*/5 * * * * /usr/local/bin/monitor_webhooks.sh
Troubleshooting
Issue: "API key not found" or "Invalid API user"
Cause: API user doesn't exist or credentials are wrong.
Solution:
# Verify user exists
mysql -u root -p asterisk -e "SELECT user, api_key, api_enabled FROM vicidial_user WHERE user='api_user';"
# Regenerate key if needed
mysql -u root -p asterisk -e "UPDATE vicidial_user SET api_key=MD5(CONCAT(NOW(), RAND())) WHERE user='api_user';"
# Retrieve new key
mysql -u root -p asterisk -N -e "SELECT api_key FROM vicidial_user WHERE user='api_user';"
Issue: Click-to-call initiates but doesn't ring agent
Cause: Agent phone not registered or campaign misconfigured.
Solution:
# Check agent registration status
asterisk -rx "sip show peers" | grep -i agent_001
# Verify campaign exists and is active
mysql -u root -p asterisk -e "SELECT campaign_id, campaign_name, active FROM vicidial_campaigns WHERE campaign_id='1234567890';"
# Check Asterisk dialplan
asterisk -rx "dialplan show from-internal-vicidial"
Issue: Webhooks not being triggered
Cause: Webhook endpoint unreachable or misconfigured in dialplan.
Solution:
# Test endpoint manually
curl -X POST https://crm.example.com/webhook/vicidial \
-d "event=test&phone=18005551234" \
-v
# Check if curl is available on ViciDial server
which curl
which wget
# Verify firewall allows outbound HTTPS
telnet crm.example.com 443
Issue: Slow API responses or timeouts
Cause: Database lock, network latency, or high server load.
Solution:
# Check MySQL load
mysqladmin -u root -p status | grep Queries
# Monitor Asterisk processes
ps aux | grep asterisk
# Check network latency
ping -c 5 crm.example.com
mtr -c 100 crm.example.com
# Increase API timeout in curl calls
curl --max-time 10 "http://vicidial.example.com/agc/api.php?..."
Issue: Call recordings not being captured
Cause: Monitor directory not writable or MixMonitor misconfigured.
Solution:
# Verify monitor directory
ls -la /var/spool/asterisk/monitor/
chmod 777 /var/spool/asterisk/monitor/
# Check dialplan for MixMonitor
grep -r "MixMonitor" /etc/asterisk/
# Restart Asterisk
service asterisk restart
Summary
You now have a complete, production-grade ViciDial API integration:
Click-to-Call Setup: Created API user, implemented JavaScript and PHP click-to-call handlers, and tested dialing workflows.
Webhook Architecture: Configured outbound webhooks to POST call events to external CRM systems, with JSON payload parsing and database logging.
Advanced Queries: Built real-time agent status queries, batch lead imports, and call recording retrieval.
Asterisk Integration: Extended the dialplan with webhook triggers, AGI scripts, and custom logging.
Monitoring & Debugging: Set up logging, connectivity tests, and error tracking.
Next Steps:
- Deploy webhook receiver to production with HTTPS
- Implement rate limiting (100 req/min per API key)
- Add retry logic for failed webhook deliveries
- Set up monitoring alerts for API errors
- Test failover to backup ViciDial server
- Document API contracts with CRM team
Security Checklist:
- API keys stored in environment variables, not hardcoded
- Webhook endpoint requires HTTPS
- CORS headers restricted to known origins
- IP whitelisting enabled for API calls
- Rate limiting implemented
- Sensitive data (recordings, notes) not logged unnecessarily
- Regular security audits of dialplan and AGI scripts
This foundation enables seamless integration between ViciDial and external business systems, automating call workflows and enriching CRM data in real-time.