@vicistack/asterisk-manager-interface-guide
v1.0.0
Published
Asterisk Manager Interface (AMI): The Complete Developer Guide — ViciStack call center engineering guide
Maintainers
Readme
Asterisk Manager Interface (AMI): The Complete Developer Guide
Last updated: March 2026 | Reading time: ~24 minutes The Asterisk Manager Interface is a TCP socket API that lets you control Asterisk programmatically. Originate calls. Monitor channels. Transfer calls. Get real-time events for every call that enters, bridges, and hangs up on your system. If you're building anything on top of Asterisk — a CRM integration, a wallboard, a click-to-call feature, a custom reporting tool — AMI is probably how you're going to do it. VICIdial itself uses AMI heavily. The agent screen, the real-time report, the auto-dial engine — they all talk to Asterisk through AMI. This guide covers everything from initial setup to production-grade integrations. No fluff. Code that works. --- ## AMI vs. ARI vs. AGI: Which One Do You Need? Asterisk has three interfaces for external programs. People confuse them constantly. AMI (Manager Interface) — TCP socket on port 5038. Send commands, receive events. Best for: monitoring, originating calls, managing channels, building dashboards. Think of it as the "admin remote control." AGI (Asterisk Gateway Interface) — Runs a script during a live call. The dialplan hits an AGI application, which forks a process (or connects to a FastAGI server), and that process controls the call step by step. Best for: IVR logic, call routing decisions, database lookups during a call. Think of it as "dialplan scripting." ARI (Asterisk REST Interface) — HTTP/WebSocket API introduced in Asterisk 12. Best for: building entire telephony applications where you want full control of every call. Stasis applications. Think of it as "build your own PBX." For VICIdial integrations, AMI is what you want 95% of the time. VICIdial doesn't use ARI. AGI is used for specific call-flow customizations. But AMI is the backbone. --- ## Setting Up manager.conf The AMI configuration lives in /etc/asterisk/manager.conf. On a fresh Asterisk install, it's usually there but disabled. ini ; /etc/asterisk/manager.conf [general] enabled = yes port = 5038 bindaddr = 127.0.0.1 ; IMPORTANT: Only listen on localhost by default displayconnects = yes timestampevents = yes [vicistack] secret = a_strong_random_password_here deny = 0.0.0.0/0.0.0.0 permit = 127.0.0.1/255.255.255.255 read = system,call,log,verbose,agent,user,config,dtmf,reporting,cdr,dialplan write = system,call,agent,user,config,command,reporting,originate writetimeout = 5000 Let's break down the important parts. ### bindaddr This controls what IP address AMI listens on. Never set this to 0.0.0.0 on a production system unless you have firewall rules restricting access. AMI has no rate limiting, no brute force protection, and if someone gets in, they can originate calls, execute shell commands (yes, really), and read your entire Asterisk configuration. ini ; GOOD: Only local connections bindaddr = 127.0.0.1 ; ACCEPTABLE: Specific internal network interface bindaddr = 10.0.0.5 ; DANGEROUS: Listen on all interfaces ; bindaddr = 0.0.0.0 If you need remote AMI access (e.g., your web server is on a different box than Asterisk), use an SSH tunnel or a VPN. Don't expose port 5038 to the internet. ### User Permissions The read and write lines control what the AMI user can do. These are comma-separated permission classes: | Class | Read (receive events) | Write (send actions) | |-------|----------------------|---------------------| | system | System events | Restart, shutdown | | call | Call events | Originate, redirect, hangup | | log | Log events | — | | verbose | Verbose messages | — | | agent | Agent events | Agent login/logoff | | user | User events | UserEvent | | config | — | GetConfig, UpdateConfig | | dtmf | DTMF events | — | | reporting | CEL/CDR events | — | | cdr | CDR events | — | | dialplan | Dialplan events | — | | command | — | Execute CLI commands | | originate | — | Originate calls | Principle of least privilege. If your integration only needs to monitor calls, give it read = call,cdr and write = (nothing). If it only needs to originate calls, give it write = originate and nothing else. The command permission is particularly dangerous — it lets the AMI user run arbitrary Asterisk CLI commands, some of which can execute shell commands. Only grant command to trusted integrations running on the same server. ### IP Restrictions Always use deny and permit to restrict which IPs can connect: ini [monitoring_user] secret = monitor_password_here deny = 0.0.0.0/0.0.0.0 ; Deny everything first permit = 127.0.0.1/255.255.255.255 ; Allow localhost permit = 10.0.1.50/255.255.255.255 ; Allow the monitoring server read = call,cdr,agent write = After editing manager.conf, reload: bash asterisk -rx "manager reload" --- ## Connecting and Authenticating AMI is a plain TCP socket. You can test it with telnet: bash telnet localhost 5038 You'll see: Asterisk Call Manager/7.0.3 Now authenticate: Action: Login Username: vicistack Secret: a_strong_random_password_here (Note the blank line at the end — AMI uses blank lines as message terminators.) Response: Response: Success Message: Authentication accepted You're in. Now you can send actions and receive events. ### Authentication Failure If you get: Response: Error Message: Authentication failed Check: 1. Username matches a section name in manager.conf (case-sensitive) 2. Secret matches exactly 3. Your IP is in the permit list 4. You reloaded manager after editing the config --- ## Core AMI Actions ### Originate — Make a Call The most used AMI action. This tells Asterisk to call a number and connect it to an extension, application, or another number. Action: Originate Channel: SIP/carrier/18005551234 Context: from-internal Exten: 100 Priority: 1 CallerID: "Sales" <18005559999> Timeout: 30000 ActionID: call-001 This tells Asterisk: "Call 18005551234 through the SIP trunk named 'carrier'. When they answer, send them to extension 100 in the from-internal context." For click-to-call (agent clicks a number in CRM, phone rings, then the lead is dialed): Action: Originate Channel: SIP/agent_phone_100 Context: click-to-call Exten: 18005551234 Priority: 1 CallerID: "Agent Smith" <18005559999> Timeout: 30000 ActionID: c2c-agent100-001 This calls the agent's phone first. When the agent picks up, Asterisk dials the lead number. ### Status — Get Channel Information Action: Status ActionID: status-001 Returns a list of all active channels with caller ID, state, duration, context, and extension. This is what powers real-time wallboards. ### Command — Run CLI Commands Action: Command Command: sip show peers ActionID: cmd-001 Returns the output of the Asterisk CLI command. Use this sparingly — it's a text-based response that you have to parse. For structured data, use dedicated actions instead. ### CoreShowChannels — Active Call Details Action: CoreShowChannels ActionID: channels-001 Returns structured data about every active channel. Better than Action: Command / Command: core show channels because the output is event-based and parseable. ### Hangup — Kill a Channel Action: Hangup Channel: SIP/carrier-00000042 ActionID: hangup-001 Immediately hangs up the specified channel. Use the channel name from Status or CoreShowChannels. ### Redirect — Transfer a Call Action: Redirect Channel: SIP/agent_100-00000042 Context: transfer-destination Exten: 200 Priority: 1 ActionID: xfer-001 Transfers the call to extension 200 in the transfer-destination context. This is how VICIdial's blind transfer works under the hood. ### QueueStatus — Queue Monitoring Action: QueueStatus ActionID: queue-001 Returns members and callers in all queues. If you're building an inbound call center dashboard, this is the action you'll use most. --- ## AMI Events: Real-Time Call Data When you connect with read permissions, Asterisk streams events to your connection in real time. Here are the events you'll see most: ### Newchannel Fired when a new channel is created (call starts): Event: Newchannel Privilege: call,all Channel: SIP/carrier-00000042 ChannelState: 0 ChannelStateDesc: Down CallerIDNum: 18005551234 CallerIDName: John Doe AccountCode: Exten: s Context: from-carrier Uniqueid: 1711408200.42 ### Dial Fired when one channel dials another: Event: Dial SubEvent: Begin Channel: SIP/carrier-00000042 Destination: SIP/agent_100-00000043 CallerIDNum: 18005551234 CallerIDName: John Doe DialString: agent_100 ### Bridge Fired when two channels are connected (call answered): Event: Bridge Privilege: call,all Bridgestate: Link Bridgetype: core Channel1: SIP/carrier-00000042 Channel2: SIP/agent_100-00000043 Uniqueid1: 1711408200.42 Uniqueid2: 1711408200.43 CallerID1: 18005551234 CallerID2: 100 ### Hangup Fired when a channel is destroyed: Event: Hangup Channel: SIP/carrier-00000042 Uniqueid: 1711408200.42 CallerIDNum: 18005551234 Cause: 16 Cause-txt: Normal Clearing ### AgentCalled / AgentConnect / AgentComplete Queue-specific events that track agent availability and call handling. Essential for building agent performance dashboards. --- ## Building a Real AMI Client Telnet is fine for testing. For production, you need a real client with connection management, event parsing, and error handling. ### Python Example python import socket import time class AMIClient: def __init__(self, host='127.0.0.1', port=5038): self.host = host self.port = port self.sock = None self.buffer = '' def connect(self): self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.sock.settimeout(30) self.sock.connect((self.host, self.port)) # Read the banner banner = self._read_response() return banner def login(self, username, secret): self._send_action({ 'Action': 'Login', 'Username': username, 'Secret': secret, }) response = self._read_response() if 'Authentication accepted' not in response: raise Exception(f'AMI login failed: {response}') return response def originate(self, channel, context, exten, priority=1, callerid=None, timeout=30000, action_id=None): action = { 'Action': 'Originate', 'Channel': channel, 'Context': context, 'Exten': exten, 'Priority': str(priority), 'Timeout': str(timeout), } if callerid: action['CallerID'] = callerid if action_id: action['ActionID'] = action_id self._send_action(action) return self._read_response() def command(self, cmd, action_id=None): action = { 'Action': 'Command', 'Command': cmd, } if action_id: action['ActionID'] = action_id self._send_action(action) return self._read_response() def logoff(self): self._send_action({'Action': 'Logoff'}) def _send_action(self, action): msg = '' for key, value in action.items(): msg += f'{key}: {value}\r\n' msg += '\r\n' self.sock.sendall(msg.encode('utf-8')) def _read_response(self): while '\r\n\r\n' not in self.buffer: chunk = self.sock.recv(4096).decode('utf-8') if not chunk: raise ConnectionError('AMI connection closed') self.buffer += chunk response, self.buffer = self.buffer.split('\r\n\r\n', 1) return response def close(self): if self.sock: self.logoff() self.sock.close() # Usage ami = AMIClient('127.0.0.1', 5038) ami.connect() ami.login('vicistack', 'your_password') # Check SIP peers result = ami.command('sip show peers') print(result) # Originate a call ami.originate( channel='SIP/carrier/18005551234', context='from-internal', exten='100', callerid='"Sales" <18005559999>', action_id='test-call-001' ) ami.close() ### Node.js Example javascript const net = require('net'); class AMIClient { constructor(host = '127.0.0.1', port = 5038) { this.host = host; this.port = port; this.socket = null; this.buffer = ''; this.callbacks = {}; this.eventHandlers = {}; } connect() { return new Promise((resolve, reject) => { this.socket = net.createConnection(this.port, this.host); this.socket.setEncoding('utf8'); this.socket.once('data', (data) => { // First data is the banner resolve(data.trim()); }); this.socket.on('data', (data) => { this.buffer += data; this._processBuffer(); }); this.socket.on('error', reject); }); } login(username, secret) { return this.sendAction({ Action: 'Login', Username: username, Secret: secret, }); } originate(channel, context, exten, options = {}) { return this.sendAction({ Action: 'Originate', Channel: channel, Context: context, Exten: exten, Priority: options.priority || '1', Timeout: options.timeout || '30000', CallerID: options.callerId || '', ActionID: options.actionId || `orig-${Date.now()}`, }); } sendAction(action) { return new Promise((resolve) => { const actionId = action.ActionID || `act-${Date.now()}`; action.ActionID = actionId; this.callbacks[actionId] = resolve; let msg = ''; for (const [key, value] of Object.entries(action)) { if (value) msg += `${key}: ${value}\r\n`; } msg += '\r\n'; this.socket.write(msg); }); } on(eventName, handler) { if (!this.eventHandlers[eventName]) { this.eventHandlers[eventName] = []; } this.eventHandlers[eventName].push(handler); } _processBuffer() { const messages = this.buffer.split('\r\n\r\n'); this.buffer = messages.pop(); // Keep incomplete message for (const msg of messages) { if (!msg.trim()) continue; const parsed = {}; for (const line of msg.split('\r\n')) { const idx = line.indexOf(': '); if (idx > 0) { parsed[line.substring(0, idx)] = line.substring(idx + 2); } } // Route to callback or event handler if (parsed.ActionID && this.callbacks[parsed.ActionID]) { this.callbacks[parsed.ActionID](parsed); delete this.callbacks[parsed.ActionID]; } if (parsed.Event) { const handlers = this.eventHandlers[parsed.Event] || []; for (const h of handlers) h(parsed); } } } close() { if (this.socket) { this.socket.write('Action: Logoff\r\n\r\n'); this.socket.end(); } } } // Usage async function main() { const ami = new AMIClient('127.0.0.1', 5038); await ami.connect(); const loginResult = await ami.login('vicistack', 'your_password'); console.log('Login:', loginResult); // Listen for call events ami.on('Newchannel', (event) => { console.log('New call:', event.CallerIDNum, event.Channel); }); ami.on('Hangup', (event) => { console.log('Call ended:', event.Channel, event['Cause-txt']); }); // Keep running to receive events // ami.close() when done } main().catch(console.error); --- ## AMI and VICIdial: How They Work Together VICIdial's relationship with AMI is deep. Here's what's happening behind the scenes: Auto-dial engine (VDauto_dial.pl): Connects to AMI and sends Originate actions for each lead pulled from the hopper. It monitors Newchannel, Bridge, and Hangup events to track call state. Agent interface (vicidial.php): Uses AMI to transfer calls, hang up channels, initiate
About
Built by ViciStack — enterprise VoIP and call center infrastructure.
- VICIdial Hosting & Optimization
- Call Center Performance Guides
- Full Article: Asterisk Manager Interface (AMI): The Complete Developer Guide
License
MIT
