Emergency server help: get in touch

Asterisk AMI Originate a Call with Python

Create a least-privilege Asterisk AMI user, send Login, Originate and Logoff, and place a call with a standard-library Python script. Tested on Asterisk 22.

Published 12 min read

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.

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 = call but not originate got Response: Error with Message: Permission denied.
  • Our write = originate user trying Application: System got Originate Access Forbidden: app or data blacklisted. Asterisk refuses shell-type applications unless the user also has the system class. 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:

ReasonWhat happened in our test
4Answered (Response: Success)
5Busy: the extension ran Busy()
8Congestion: the extension ran Congestion()
3Rang until the Originate Timeout (3000 ms) expired: no answer
0Failed: 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.1 and 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 permit line 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 originate and call.
  • 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. Check asterisk -rx "manager show settings".
  • Authentication failed: wrong username or secret, or your source IP is not in permit. Did you run manager reload?
  • Permission denied on Originate: the user lacks write = originate.
  • Script waits and never prints OriginateResponse: the user lacks read = call, or you logged in with Events: 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.

Free website test

Is your website set up right?

Check SSL, security headers, redirects, robots.txt, sitemap, llms.txt and security.txt in one test. It takes about 30 seconds.