← All Tutorials

ViciDial API Integration — Click-to-Call & CRM Webhooks

ViciDial Administration Intermediate 13 min read #57

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:

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)

2. Webhook System

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:

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:

  1. User/agent clicks button in CRM
  2. JavaScript sends HTTP request to ViciDial API
  3. API validates caller, creates call record
  4. Asterisk dials agent's phone
  5. Once answered, Asterisk dials customer
  6. Bridge established; call logged

API Endpoint: /agc/api.php

Method: GET or POST
Required parameters:

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:

  1. Click-to-Call Setup: Created API user, implemented JavaScript and PHP click-to-call handlers, and tested dialing workflows.

  2. Webhook Architecture: Configured outbound webhooks to POST call events to external CRM systems, with JSON payload parsing and database logging.

  3. Advanced Queries: Built real-time agent status queries, batch lead imports, and call recording retrieval.

  4. Asterisk Integration: Extended the dialplan with webhook triggers, AGI scripts, and custom logging.

  5. Monitoring & Debugging: Set up logging, connectivity tests, and error tracking.

Next Steps:

Security Checklist:

This foundation enables seamless integration between ViciDial and external business systems, automating call workflows and enriching CRM data in real-time.

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