Short answer: Add an AMI user with write = originate and read = call, limited to 127.0.0.1, in manager_custom.conf (FreePBX) or manager.conf (plain Asterisk), then reload the manager. Your program connects to TCP port 5038, sends Action: Login, then Action: Originate with Async: true, waits for the OriginateResponse event with the same ActionID, and sends Action: Logoff. The Python script below does this with the standard library only. Never expose port 5038 to the internet.
We ran the AMI login, Originate and Logoff steps and the Python script below on our lab server (Debian 12, Asterisk 22.11) on 7 October 2026. To keep the lab PBX unchanged and avoid ringing any phone or trunk, we ran them against a separate throwaway Asterisk 22.11 instance started from a test folder, with its own AMI user on 127.0.0.1 and Local channel test extensions. The FreePBX instance’s AMI configuration was not changed.
Table of Contents
How AMI and Originate work
The Asterisk Manager Interface (AMI) is a plain-text protocol over TCP, port 5038 by default. Each message is a set of Key: Value lines ending in CRLF, and a blank line ends the message. When you connect, Asterisk sends one banner line; on Asterisk 22.11 it was Asterisk Call Manager/11.0.0 (the AMI protocol version, not the Asterisk version).
Three kinds of message come back: a Response to each action, Events about channels, and event lists. Every action you send can carry an ActionID; Asterisk copies it into the Response and into related events, which is how your code matches answers to questions.
Originate creates a new channel (for example a phone) and, when it answers, connects it either to a dialplan location (Context, Exten, Priority) or to an application (Application, Data). With Async: true, Asterisk replies straight away with “Originate successfully queued” and later sends an OriginateResponse event with the result. Without it, the Response arrives only when the call is answered or fails, and your connection is blocked meanwhile.
Create a least-privilege AMI user
On FreePBX, manager.conf is generated and ends with #include manager_additional.conf and #include manager_custom.conf, so put your user in /etc/asterisk/manager_custom.conf. Do not reuse the AMI user FreePBX creates for itself. On plain Asterisk, add the section to /etc/asterisk/manager.conf and make sure [general] has enabled = yes and bindaddr = 127.0.0.1 (FreePBX already binds to 127.0.0.1 on our lab).
; /etc/asterisk/manager_custom.conf
[click2call]
secret = PASTE-A-LONG-RANDOM-STRING-HERE
deny = 0.0.0.0/0.0.0.0
permit = 127.0.0.1/255.255.255.255
read = call
write = originate
Generate the secret with openssl rand -hex 24, then apply and check the user:
chown asterisk:asterisk /etc/asterisk/manager_custom.conf
chmod 640 /etc/asterisk/manager_custom.conf
asterisk -rx "manager reload"
asterisk -rx "manager show user click2call"
Why these classes: manager show command Originate on Asterisk 22.11 lists the privilege as originate,all, so write = originate is enough to send it. The OriginateResponse event and the channel events are in the call class (they arrived with Privilege: call,all in our test), so read = call lets you see the result. In our test:
- A user with
write = callbut notoriginategotResponse: ErrorwithMessage: Permission denied. - Our
write = originateuser tryingApplication: SystemgotOriginate Access Forbidden: app or data blacklisted. Asterisk refuses shell-type applications unless the user also has thesystemclass. Do not grant it to a click-to-call user.
The raw AMI conversation
This is a real exchange from our lab test, trimmed. The secret is masked, and several channel events (Newchannel, DialBegin, Newstate, Hangup) are cut from the middle. Lines we sent start with Action:
Asterisk Call Manager/11.0.0
Action: Login
ActionID: 1
Username: w6-originate
Secret: ********
Events: call
Response: Success
ActionID: 1
Message: Authentication accepted
Action: Originate
ActionID: 2
Channel: Local/100@w6test
Application: Wait
Data: 1
Async: true
Response: Success
ActionID: 2
Message: Originate successfully queued
Event: OriginateResponse
Privilege: call,all
ActionID: 2
Response: Success
Channel: Local/100@w6test-00000003;1
Application: Wait
Data: 1
Reason: 4
Uniqueid: 1791349572.6
CallerIDNum: <unknown>
CallerIDName: <unknown>
Action: Logoff
ActionID: 3
Response: Goodbye
ActionID: 3
Message: Thanks for all the fish.
Events: call in the Login limits which events this session receives. Without it you get every class your user may read, which on a busy PBX is a lot of text to skip.
Python 3 script: originate a call with the standard library
The script below uses only socket, argparse and uuid. It reads the secret from the AMI_SECRET environment variable (never a command-line argument, which other users can see in ps), tags each action with a random ActionID, skips unrelated events, and waits for the matching OriginateResponse. Exit codes: 0 answered, 2 login failed, 3 Originate refused, 4 call failed.
#!/usr/bin/env python3
"""ami_originate.py - place a call through the Asterisk Manager Interface (AMI).
Standard library only (Python 3.8+). Logs in, sends an async Originate,
waits for the matching OriginateResponse event, then logs off.
The AMI secret is read from the AMI_SECRET environment variable, never from
the command line (command lines are visible in `ps`).
Asterisk AMI Originate a Call with Python
License: MIT
"""
import argparse
import os
import socket
import sys
import uuid
class AMIError(Exception):
pass
class AMI:
def __init__(self, host, port, timeout=10):
self.sock = socket.create_connection((host, port), timeout=timeout)
self.buf = b''
banner = self._readline()
if not banner.startswith('Asterisk Call Manager'):
raise AMIError('unexpected banner: %r' % banner)
self.banner = banner
def _readline(self):
while b'\r\n' not in self.buf:
chunk = self.sock.recv(4096)
if not chunk:
raise AMIError('connection closed by Asterisk')
self.buf += chunk
line, self.buf = self.buf.split(b'\r\n', 1)
return line.decode('utf-8', 'replace')
def read_message(self):
"""Return one AMI message (Response or Event) as a dict."""
msg = {}
while True:
line = self._readline()
if line == '':
if msg:
return msg
continue
key, _, value = line.partition(':')
msg[key.strip()] = value.strip()
def send(self, action, **fields):
action_id = fields.pop('ActionID', None) or uuid.uuid4().hex
lines = ['Action: %s' % action, 'ActionID: %s' % action_id]
for key, value in fields.items():
if isinstance(value, (list, tuple)): # e.g. several Variable: lines
lines += ['%s: %s' % (key, v) for v in value]
elif value is not None:
lines.append('%s: %s' % (key, value))
self.sock.sendall(('\r\n'.join(lines) + '\r\n\r\n').encode('utf-8'))
return action_id
def wait_for(self, action_id, event=None):
"""Read until the Response (event=None) or the named Event for action_id."""
while True:
msg = self.read_message()
if msg.get('ActionID') != action_id:
continue # unrelated event, skip it
if event is None and 'Response' in msg:
return msg
if event is not None and msg.get('Event') == event:
return msg
def close(self):
try:
self.sock.close()
except OSError:
pass
def main():
ap = argparse.ArgumentParser(description='Originate a call via Asterisk AMI.')
ap.add_argument('--host', default='127.0.0.1')
ap.add_argument('--port', type=int, default=5038)
ap.add_argument('--user', required=True, help='AMI username from manager_custom.conf')
ap.add_argument('--channel', required=True, help='e.g. PJSIP/1001 or Local/1001@from-internal')
ap.add_argument('--context', help='dialplan context for the answered call')
ap.add_argument('--exten', help='extension in --context')
ap.add_argument('--priority', default='1')
ap.add_argument('--application', help='run an application instead of dialplan, e.g. Playback')
ap.add_argument('--data', help='application data, e.g. demo-congrats')
ap.add_argument('--callerid', help='e.g. "Click to call" <1001>')
ap.add_argument('--ring-timeout', type=int, default=30, help='seconds to wait for an answer')
args = ap.parse_args()
secret = os.environ.get('AMI_SECRET')
if not secret:
sys.exit('Set AMI_SECRET in the environment (not on the command line).')
if bool(args.application) == bool(args.context and args.exten):
sys.exit('Give either --context and --exten, or --application (with optional --data).')
ami = AMI(args.host, args.port, timeout=args.ring_timeout + 15)
print('Connected:', ami.banner)
try:
r = ami.wait_for(ami.send('Login', Username=args.user, Secret=secret, Events='call'))
print('Login:', r.get('Response'), '-', r.get('Message'))
if r.get('Response') != 'Success':
return 2
fields = dict(Channel=args.channel, CallerID=args.callerid, Async='true',
Timeout=str(args.ring_timeout * 1000))
if args.application:
fields.update(Application=args.application, Data=args.data)
else:
fields.update(Context=args.context, Exten=args.exten, Priority=args.priority)
aid = ami.send('Originate', **fields)
r = ami.wait_for(aid)
print('Originate:', r.get('Response'), '-', r.get('Message'))
if r.get('Response') != 'Success':
return 3
ev = ami.wait_for(aid, event='OriginateResponse')
print('OriginateResponse:', ev.get('Response'), 'Reason=%s' % ev.get('Reason'),
'Channel=%s' % ev.get('Channel'), 'Uniqueid=%s' % ev.get('Uniqueid'))
return 0 if ev.get('Response') == 'Success' else 4
finally:
try:
r = ami.wait_for(ami.send('Logoff'))
print('Logoff:', r.get('Response'), '-', r.get('Message'))
except (AMIError, OSError):
pass
ami.close()
if __name__ == '__main__':
try:
sys.exit(main())
except (AMIError, OSError) as e:
sys.exit('AMI error: %s' % e)
Save it as ami_originate.py. Two ways to call it on FreePBX:
export AMI_SECRET='the-secret-from-manager_custom.conf'
# ring extension 1001; when answered, dial 1002 through the normal dialplan
python3 ami_originate.py --user click2call --channel PJSIP/1001 \
--context from-internal --exten 1002 --callerid '"Click to call" <1001>'
# ring extension 1001 and play a sound file to it
python3 ami_originate.py --user click2call --channel PJSIP/1001 \
--application Playback --data demo-congrats
Output from our lab runs against the test instance (port 5039 there, Local channel to a test extension):
Connected: Asterisk Call Manager/11.0.0
Login: Success - Authentication accepted
Originate: Success - Originate successfully queued
OriginateResponse: Success Reason=4 Channel=Local/100@w6test-00000000;1 Uniqueid=1791349553.0
Logoff: Goodbye - Thanks for all the fish.
An Originate into from-internal with an external number uses your outbound routes and costs money. If the script is reachable from a web page, validate the destination against an allow-list, rate-limit it, and read our toll fraud checklist first.
Reading the OriginateResponse Reason
Response is Success or Failure; Reason says why. These are the values we got in our lab test, using test extensions that answer, return busy, return congestion, keep ringing past the timeout, or do not exist:
| Reason | What happened in our test |
|---|---|
| 4 | Answered (Response: Success) |
| 5 | Busy: the extension ran Busy() |
| 8 | Congestion: the extension ran Congestion() |
| 3 | Rang until the Originate Timeout (3000 ms) expired: no answer |
| 0 | Failed: the extension did not exist (Uniqueid was <unknown>) |
Timeout is in milliseconds. Our script sends --ring-timeout seconds multiplied by 1000.
Security rules for AMI
- Never open 5038 to the internet. AMI sends the secret and all call data in clear text. Keep
bindaddr = 127.0.0.1and block the port in the firewall too; see SIP ports firewall rules. - Remote apps: run your code on the PBX, or reach AMI through an SSH tunnel or VPN. If you must connect across a network, use AMI over TLS and a
permitline for that one host only. - One user per application, with only the classes it needs. A reporting tool needs read classes only; click-to-call needs
originateandcall. - Protect the secret: file mode 640 or tighter, environment variable or a root-only file for your script, never in a web root or a Git repository.
- Watch failed logins: wrong secrets return
Authentication failed. On FreePBX the Asterisk security log channel feeds fail2ban, and fail2ban for Asterisk can block repeat offenders.
Check that it worked, and common problems
While testing, open the Asterisk CLI (asterisk -rvvv) in a second terminal: you will see the AMI login and the new channels. asterisk -rx "manager show connected" lists current AMI sessions. The call also appears in CDR.
Connection refused: AMI is disabled, bound to another address, or you used the wrong port. Checkasterisk -rx "manager show settings".Authentication failed: wrong username or secret, or your source IP is not inpermit. Did you runmanager reload?Permission deniedon Originate: the user lackswrite = originate.- Script waits and never prints OriginateResponse: the user lacks
read = call, or you logged in withEvents: off. - Failure with Reason 0: wrong channel name, wrong context or extension, or the endpoint is not registered. Check with
asterisk -rx "pjsip show endpoints"and this guide for unreachable endpoints.
Official documentation: Asterisk: Originate AMI action · Asterisk: The Manager TCP/IP API · Python: socket module
Related: Asterisk CLI Commands Cheat Sheet: PJSIP, Calls, Dialplan, Logs · Asterisk and FreePBX Toll Fraud Prevention: 10-Point Checklist · fail2ban for Asterisk and FreePBX: Block SIP Password Guessing · Asterisk AudioSocket: Connect a Call to an AI Voice Agent · Asterisk Dialplan Pattern Tester: Which Extension Matches?
See also: Asterisk pjsip_wizard.conf: Endpoints and Trunks in a Few Lines · Asterisk CDR Reports from MySQL/MariaDB (FreePBX asteriskcdrdb)
Frequently asked questions
What permissions does an AMI user need to originate calls?
write = originate to send the Originate action, and read = call to receive the OriginateResponse event. Applications such as System need the system class as well, which you should avoid.
What is the difference between Async true and false in Originate?
With Async true Asterisk replies at once and reports the result later in an OriginateResponse event. With Async false the Response waits until the call is answered or fails.
Is the Timeout in Originate seconds or milliseconds?
Milliseconds. 30000 means 30 seconds of ringing before the attempt fails with no answer.
Can I use AMI on FreePBX without breaking FreePBX?
Yes. Add your own user in manager_custom.conf, reload the manager and leave the FreePBX-generated user and manager.conf alone.
Should I use ARI instead of AMI?
AMI is fine for originating calls and watching events. ARI (REST and WebSocket) suits applications that control the call media and flow themselves.
Is it safe to open port 5038 to my office IP?
It is risky because AMI is clear text. Use an SSH tunnel, a VPN or AMI over TLS instead, and keep the permit list to single hosts.