# Asterisk AMI Originate a Call with Python

Source: https://srvscripts.com/guides/asterisk-ami-originate-call-python/
Updated: 2026-10-07
Publisher: srvScripts (https://srvscripts.com/)

**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:
CallerIDName:

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" ')
    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" '

# 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](/guides/asterisk-freepbx-toll-fraud-prevention/) 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 ) |

`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](/guides/sip-ports-firewall/).

- **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](/guides/fail2ban-asterisk-freepbx/) 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](/guides/pjsip-endpoint-unreachable-qualify/).

**Official documentation:** [Asterisk: Originate AMI action](https://docs.asterisk.org/Latest_API/API_Documentation/AMI_Actions/Originate/) · [Asterisk: The Manager TCP/IP API](https://docs.asterisk.org/Configuration/Interfaces/Asterisk-Manager-Interface-AMI/The-Asterisk-Manager-TCP-IP-API/) · [Python: socket module](https://docs.python.org/3/library/socket.html)

**Related:** [Asterisk CLI Commands Cheat Sheet: PJSIP, Calls, Dialplan, Logs](/guides/asterisk-cli-commands-cheat-sheet/) · [Asterisk and FreePBX Toll Fraud Prevention: 10-Point Checklist](/guides/asterisk-freepbx-toll-fraud-prevention/) · [fail2ban for Asterisk and FreePBX: Block SIP Password Guessing](/guides/fail2ban-asterisk-freepbx/) · [Asterisk AudioSocket: Connect a Call to an AI Voice Agent](/guides/asterisk-audiosocket-ai-voice-agent/) · [Asterisk Dialplan Pattern Tester: Which Extension Matches?](/tools/dialplan-pattern-tester/)

**See also:** [Asterisk pjsip_wizard.conf: Endpoints and Trunks in a Few Lines](/guides/asterisk-pjsip-wizard/) · [Asterisk CDR Reports from MySQL/MariaDB (FreePBX asteriskcdrdb)](/guides/asterisk-cdr-mysql-reporting/)

## 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.
