AGI, ARI and AMI Integration
Activate this skill when the user needs external code to control or observe an Asterisk PBX: a database dip during a call, a custom IVR or dialer written in a real language, a wallboard, click-to-call, or agent state control. Triggers on "AGI," "FastAGI," "ARI," "Stasis," "AMI," "manager.conf," "ari.conf," "Originate," "asterisk REST," "asterisk websocket," "asterisk events," or "asterisk integration." Covers choosing between the three interfaces, authentication for each, a small working example of each, and the performance mistakes that take production systems down.
You are a VoIP engineer who has integrated Asterisk 16 through 21 with CRMs, dialers, wallboards and custom IVRs for call centres and SIP trunking providers. You have replaced per-call AGI scripts that forked a Python interpreter 40 times a second with a FastAGI daemon, watched an AMI consumer fall behind and get disconnected during a call spike, and rebuilt an ARI application after learning what happens to channels when the WebSocket drops. You choose the interface by the shape of the problem, and you measure before deploying.
## Key Points
1. Write the requirement in one sentence. "Look up X during the call" is AGI. "Build a flow that does A then B depending on C" is ARI. "Display or control what is happening" is AMI.
2. Estimate calls per second at peak. Above a handful, AGI must be FastAGI or a `func_odbc` lookup instead.
3. Decide where the code runs: on the PBX host (AGI, localhost ARI/AMI) or elsewhere (FastAGI, ARI/AMI over a private network with TLS).
4. Create a dedicated user per application with the least read/write classes (AMI) or `read_only = yes` where possible (ARI).
5. Instrument: log every action and its latency; alert when AMI write buffers or ARI event lag grow.
6. Test the failure mode: kill the application mid-call and confirm the caller ends up somewhere sensible.
- **AGI blocking the channel.** The caller hears silence while the script runs. Put a hard timeout on every network call inside it.
- **Synchronous `Originate`.** Without `Async: true` the action blocks until the call is answered or fails, holding the AMI session for up to `Timeout`.
- **`Status` and `CoreShowChannels` on a busy system** return one event per channel; poll rarely and prefer tracking state from the event stream.
- **ARI single event loop doing HTTP.** A 200 ms REST call inside the handler delays DTMF for every caller. Use an async HTTP client or worker threads.
- AMI bound to `127.0.0.1` or a private interface, `deny` then `permit`, per-user classes
- ARI behind TLS (reverse proxy or `tlsenable` in `http.conf`) if not local, `read_only` users for dashboards
## Quick Example
```ini
exten => s,1,AGI(tier-lookup.py,${ACCT})
same => n,GotoIf($["${AGISTATUS}"!="SUCCESS"]?fallback)
same => n,GotoIf($["${ACCT_TIER}"="gold"]?queue-entry,vip,1:queue-entry,sales,1)
```skilldb get asterisk-pbx-skills/agi-ari-and-ami-integrationFull skill: 210 linesAGI, ARI and AMI Integration
You are a VoIP engineer who has integrated Asterisk 16 through 21 with CRMs, dialers, wallboards and custom IVRs for call centres and SIP trunking providers. You have replaced per-call AGI scripts that forked a Python interpreter 40 times a second with a FastAGI daemon, watched an AMI consumer fall behind and get disconnected during a call spike, and rebuilt an ARI application after learning what happens to channels when the WebSocket drops. You choose the interface by the shape of the problem, and you measure before deploying.
Core Philosophy: Three Interfaces, Three Shapes of Problem
| Interface | Direction | Shape of problem | Latency model |
|---|---|---|---|
| AGI | Dialplan calls out to your script, waits, continues | "During this call, look something up and set a variable" | Synchronous; channel is blocked while the script runs |
| ARI | Your application owns channels handed to it by Stasis(); REST + WebSocket | "I am building an application that happens to use telephony" (custom IVR, dialer, conferencing, call flows with real logic) | Asynchronous events plus HTTP commands |
| AMI | Your client observes and pokes the whole system over a TCP socket | "Show me what the PBX is doing and let me act on it" (wallboards, click-to-call, agent state, CDR streaming) | Event stream plus request/response actions |
The rule: dialplan first, then AGI for a lookup, ARI when the flow itself belongs in code, AMI for anything system-wide. Do not write a queue engine in AMI or a wallboard in AGI.
AGI
AGI(script[,args]) runs /var/lib/asterisk/agi-bin/script as the asterisk user. Asterisk writes environment lines to the script's stdin (agi_request, agi_channel, agi_uniqueid, agi_callerid, agi_context, agi_extension, agi_priority, agi_arg_1...) terminated by a blank line. The script then writes commands to stdout and reads one response line per command: 200 result=<n> [data] on success, 510 for unknown command, 511 on a dead channel, 520 for bad syntax.
Commands you will actually use: ANSWER, HANGUP, GET VARIABLE name, SET VARIABLE name "value", EXEC application "args", STREAM FILE file "escape digits", GET DATA file timeout maxdigits, SAY DIGITS, RECORD FILE, WAIT FOR DIGIT, SET CONTEXT|EXTENSION|PRIORITY, DATABASE GET|PUT, VERBOSE "msg" level, CHANNEL STATUS, NOOP.
#!/usr/bin/env python3
# /var/lib/asterisk/agi-bin/tier-lookup.py -- called as AGI(tier-lookup.py,${ACCT})
import sys
env = {}
for line in sys.stdin:
line = line.strip()
if not line:
break
key, _, value = line.partition(':')
env[key.strip()] = value.strip()
def agi(cmd):
sys.stdout.write(cmd + '\n')
sys.stdout.flush()
return sys.stdin.readline().strip()
acct = ''.join(ch for ch in env.get('agi_arg_1', '') if ch.isdigit())
tier = 'gold' if acct.startswith('9') else 'standard' # replace with a real lookup
agi(f'SET VARIABLE ACCT_TIER "{tier}"')
agi(f'VERBOSE "account {acct} tier {tier}" 3')
exten => s,1,AGI(tier-lookup.py,${ACCT})
same => n,GotoIf($["${AGISTATUS}"!="SUCCESS"]?fallback)
same => n,GotoIf($["${ACCT_TIER}"="gold"]?queue-entry,vip,1:queue-entry,sales,1)
${AGISTATUS} is SUCCESS, FAILURE, NOTFOUND or HANGUP. agi set debug on prints every line both ways. Variants: EAGI exposes the channel's incoming audio on file descriptor 3; AGI(agi://host:4573/path) is FastAGI, where a daemon accepts a TCP connection per call and reads the same protocol from the socket. DeadAGI is gone; plain AGI in the h extension works with the reduced command set.
Authentication: there is none. AGI scripts run with the PBX's identity, so file permissions on agi-bin are the control, and every agi_arg_* value is caller-influenced data that must be sanitised before it touches SQL or a shell. FastAGI has no authentication either; bind it to localhost or a private network and firewall it.
ARI
Enable the HTTP server and an ARI user:
; http.conf
[general]
enabled = yes
bindaddr = 127.0.0.1
bindport = 8088
; ari.conf
[general]
enabled = yes
pretty = yes
allowed_origins = https://ops.example.com ; browsers only; server apps ignore it
[ivrapp]
type = user
read_only = no
password = 2f8b1c0d9e7a4b6c
password_format = plain
Hand a channel to your application from the dialplan with Stasis(ivrapp,inbound,${EXTEN}). Your application connects to ws://127.0.0.1:8088/ari/events?app=ivrapp&api_key=ivrapp:2f8b1c0d9e7a4b6c (or HTTP Basic on the REST side) and receives JSON events: StasisStart with the channel and the arguments, then ChannelDtmfReceived, ChannelStateChange, PlaybackFinished, RecordingFinished, ChannelHangupRequest, StasisEnd. Commands are REST calls on /ari:
B="http://127.0.0.1:8088/ari"; A="-u ivrapp:2f8b1c0d9e7a4b6c"
curl $A -X POST "$B/channels/$CH/answer"
curl $A -X POST "$B/channels/$CH/play?media=sound:custom/welcome&playbackId=welcome-$CH"
curl $A -X POST "$B/channels/$CH/variable?variable=ACCT_TIER&value=gold"
curl $A -X POST "$B/bridges?type=mixing&bridgeId=b-$CH"
curl $A -X POST "$B/bridges/b-$CH/addChannel?channel=$CH"
curl $A -X POST "$B/channels?endpoint=PJSIP/101&app=ivrapp&appArgs=agent&channelId=agent-$CH"
curl $A -X POST "$B/channels/$CH/continue?context=from-internal&extension=200&priority=1"
curl $A -X DELETE "$B/channels/$CH"
Media URIs are sound:, recording:, number:, digits:, characters: and tone:. Supplying your own channelId, bridgeId and playbackId values is what lets you correlate the StasisStart for a channel you originated with the request that created it; without them you race the event. POST /channels/create followed by POST /channels/{id}/dial (Asterisk 14 and later) originates without waiting for answer. POST /channels/{id}/snoop attaches a spy channel for whisper and barge; POST /channels/externalMedia (16.6 and later) streams a channel's audio to an RTP address, the hook for live transcription.
A minimal application loop in Python:
import json, requests, websocket # websocket-client
BASE, AUTH = "http://127.0.0.1:8088/ari", ("ivrapp", "2f8b1c0d9e7a4b6c")
def post(path, **params): return requests.post(BASE + path, auth=AUTH, params=params, timeout=5)
def on_message(ws, raw):
ev = json.loads(raw)
if ev["type"] == "StasisStart":
ch = ev["channel"]["id"]
post(f"/channels/{ch}/answer")
post(f"/channels/{ch}/play", media="sound:custom/menu-top", playbackId=f"menu-{ch}")
elif ev["type"] == "ChannelDtmfReceived":
ch, digit = ev["channel"]["id"], ev["digit"]
post(f"/playbacks/menu-{ch}/stop")
if digit == "1":
post(f"/channels/{ch}/continue", context="queue-entry", extension="sales", priority=1)
else:
post(f"/channels/{ch}/play", media="sound:pbx-invalid")
ws = websocket.WebSocketApp(
"ws://127.0.0.1:8088/ari/events?app=ivrapp&api_key=ivrapp:2f8b1c0d9e7a4b6c",
on_message=on_message)
ws.run_forever(ping_interval=20)
Keep the handler non-blocking: hand REST calls to a worker pool or an async client, because a slow HTTP call inside on_message delays every other channel's events. ari show apps, ari show app ivrapp and ari set debug all on are the CLI side.
AMI
; manager.conf
[general]
enabled = yes
port = 5038
bindaddr = 127.0.0.1
displayconnects = no
timestampevents = yes
[wallboard]
secret = 9c4e2a7b1d6f8e3a
deny = 0.0.0.0/0.0.0.0
permit = 127.0.0.1/255.255.255.255
permit = 10.20.0.0/255.255.255.0
read = call,agent,reporting
write = call,originate,agent
eventfilter = Event: Queue.*
eventfilter = Event: Agent.*
The protocol is Key: Value lines ending in a blank line, \r\n separated. Login with Action: Login, Username, Secret, and optionally Events: off for a connection that only issues actions. Every action should carry an ActionID so responses can be matched. Actions used daily: Originate (Channel, Context/Exten/Priority or Application/Data, CallerID, Timeout in milliseconds, Variable, Async: true, ChannelId), Hangup, Redirect, QueueStatus, QueueSummary, QueueAdd, QueuePause, Getvar, Setvar, CoreShowChannels, PJSIPShowEndpoints, Command (any CLI command), MixMonitor, MixMonitorMute, UserEvent, Ping.
import socket
s = socket.create_connection(("127.0.0.1", 5038))
def send(**kv):
s.sendall("".join(f"{k}: {v}\r\n" for k, v in kv.items()).encode() + b"\r\n")
send(Action="Login", Username="wallboard", Secret="9c4e2a7b1d6f8e3a", Events="off")
send(Action="Originate", ActionID="c2c-1", Channel="PJSIP/101", Context="outbound",
Exten="12125551234", Priority="1", CallerID='"Click to call" <2125550100>',
Timeout="30000", Async="true", Variable="CRM_ID=44821")
Events worth subscribing to: Newchannel, Newstate, Hangup, DialBegin, DialEnd, BridgeEnter, BridgeLeave, QueueCallerJoin, QueueCallerLeave, QueueCallerAbandon, AgentCalled, AgentConnect, AgentComplete, QueueMemberStatus, QueueMemberPause, ContactStatus, DeviceStateChange, Cdr (with cdr_manager), CEL (with cel_manager), UserEvent. manager show connected, manager show users, manager show commands and manager show command Originate are the CLI side; the last one prints the version-accurate argument list.
AMI over HTTP (webenabled = yes plus http.conf) exposes /rawman, /manager and /mxml; it exists but a persistent TCP connection is simpler and faster.
Procedure: Choosing and Deploying
- Write the requirement in one sentence. "Look up X during the call" is AGI. "Build a flow that does A then B depending on C" is ARI. "Display or control what is happening" is AMI.
- Estimate calls per second at peak. Above a handful, AGI must be FastAGI or a
func_odbclookup instead. - Decide where the code runs: on the PBX host (AGI, localhost ARI/AMI) or elsewhere (FastAGI, ARI/AMI over a private network with TLS).
- Create a dedicated user per application with the least read/write classes (AMI) or
read_only = yeswhere possible (ARI). - Instrument: log every action and its latency; alert when AMI write buffers or ARI event lag grow.
- Test the failure mode: kill the application mid-call and confirm the caller ends up somewhere sensible.
Performance Pitfalls
- AGI process spawn. Each
AGI()forks and execs; an interpreter with imports and a fresh database connection costs tens to hundreds of milliseconds and a CPU spike per call. Move to FastAGI with a persistent connection pool, or replace withODBC_*functions fromfunc_odbc.conffor simple lookups. - AGI blocking the channel. The caller hears silence while the script runs. Put a hard timeout on every network call inside it.
- AMI event floods.
VarSet,NewextenandRTCP*events dwarf everything else. Restrictread=classes and useeventfilter; a consumer that cannot keep up is disconnected when its write buffer fills, and reconnects create the same problem again. - Synchronous
Originate. WithoutAsync: truethe action blocks until the call is answered or fails, holding the AMI session for up toTimeout. StatusandCoreShowChannelson a busy system return one event per channel; poll rarely and prefer tracking state from the event stream.- ARI single event loop doing HTTP. A 200 ms REST call inside the handler delays DTMF for every caller. Use an async HTTP client or worker threads.
- ARI reconnects. When the WebSocket drops, channels in
Stasis()wait with no application; on reconnect they are still there, so replay state or hang them up deliberately. Put aHangup()or fallback route afterStasis()in the dialplan for the case where the application exits the channel. - Multiple apps, one socket. Recent releases accept a comma-separated
app=a,blist on one WebSocket; older ones need a connection per application. Confirm on your version before designing around it.
Checklist
- AMI bound to
127.0.0.1or a private interface,denythenpermit, per-user classes - ARI behind TLS (reverse proxy or
tlsenableinhttp.conf) if not local,read_onlyusers for dashboards - AGI arguments sanitised; no caller data in shell commands
- Timeouts on every outbound network call from any integration
ActionID,channelId,playbackIdused for correlation- Reconnect logic tested by restarting Asterisk under load
- Metrics on event lag and action latency
Common Mistakes
- Exposing 5038 or 8088 to the internet with a password from the sample file. Both are scanned constantly.
- Parsing AMI by line without handling the blank-line terminator, breaking on multi-line
Commandresponses. - Using AGI for the entire IVR and wondering why the PBX load average is 8 at 30 calls.
- Writing an ARI application that assumes events arrive in order across channels; they are ordered per channel, not globally.
- Forgetting that
Stasis()returns when your app callscontinue, so the next dialplan priority runs; leave aHangup()after it unless continuation is intended. - Reading
Cdrevents as the CDR of record; they are a copy for streaming, and if the consumer was disconnected the record is gone.
Limits and When Not to Use This
None of these interfaces make Asterisk a media server API for arbitrary audio processing; for streaming audio into speech systems use ARI external media or EAGI, both with real limits on concurrency. AMI is not a message bus; do not fan out its events to many consumers directly, use one proxy that republishes to a queue. If the requirement is dynamic configuration (endpoints, queues, dialplan) rather than call control, that is realtime (sorcery.conf, extconfig.conf) or generated config with reloads, not any of the three.
Install this skill directly: skilldb add asterisk-pbx-skills
Related Skills
Asterisk Architecture and Installation
Activate this skill when the user is standing up an Asterisk PBX for the first time, choosing between source and package installs, or trying to understand how channels, the dialplan, applications and modules fit together. Triggers on "asterisk," "Asterisk PBX," "install asterisk," "menuselect," "asterisk from source," "asterisk CLI," "modules.conf," "asterisk.conf," "/etc/asterisk," "chan_pjsip," or "asterisk directory layout." Covers the core architecture, Debian and RHEL installs, menuselect choices, the directory tree, and the checks to run on first boot.
Asterisk Security Hardening
Activate this skill when the user is exposing an Asterisk PBX to the internet, has just been hit by toll fraud, or wants to lock down SIP registration, the dialplan, AMI and ARI before an audit. Triggers on "asterisk security," "toll fraud," "fail2ban asterisk," "SIP brute force," "friendly-scanner," "permit deny," "acl.conf," "alwaysauthreject," "SRTP," "SIP TLS," "firewall RTP," "restrict international calls," or "Asterisk PBX hardening." Covers the fraud patterns actually seen in production, log-driven banning, ACLs, credential policy, dialplan class of service and concurrency limits, TLS and SRTP transport configuration, and firewall rules for SIP and the RTP range.
Call Queues and Agents
Activate this skill when the user is configuring queues.conf on an Asterisk PBX, choosing a ring strategy, managing agents who log in and out, tuning hold announcements, or explaining queue statistics to a call-centre manager. Triggers on "queues.conf," "app_queue," "ringall," "rrmemory," "leastrecent," "penalty," "AddQueueMember," "PauseQueueMember," "wrapuptime," "queue_log," "service level," "abandon rate," "queue show," or "asterisk call center." Covers strategies, penalties and queue rules, static and dynamic members, announcements, wrap-up, the queue_log format, the metrics that matter, and the bugs every queue deployment hits.
CDR, CEL and Reporting
Activate this skill when the user needs billing-grade call records from an Asterisk PBX: configuring cdr.conf with a CSV or ODBC backend, enabling CEL for per-event detail, adding custom fields to records, writing call reports in SQL, or reconciling minutes against a carrier invoice. Triggers on "CDR," "cdr.conf," "Master.csv," "cdr_adaptive_odbc," "CEL," "cel.conf," "linkedid," "billsec," "disposition," "call report," "ASR," "ACD," "carrier invoice," "CDR(userfield)," or "asterisk billing." Covers what each CDR field really means, backend configuration for CSV and ODBC, CEL event types and how to read them, custom variables, the SQL reports managers actually ask for, and a reconciliation method that survives rounding, time zones and Local channels.
Dialplan Programming
Activate this skill when the user is writing or debugging extensions.conf for an Asterisk PBX: routing inbound DIDs, building outbound rules, structuring contexts, or replacing Macro() with GoSub. Triggers on "asterisk dialplan," "extensions.conf," "pattern matching," "_NXXXXXX," "GoSub," "Dial options," "hangup handler," "priorities," "same => n," "include =>," "DIALSTATUS," or "dialplan reload." Covers contexts, extensions, priorities, pattern matching, variables and functions, subroutines, Dial behaviour and worked dialplans you can paste and adapt.
IVR Design and Implementation
Activate this skill when the user is building an auto-attendant or interactive voice menu on an Asterisk PBX, recording prompts, routing by business hours, or debugging callers who get stuck or hang up in a menu. Triggers on "IVR," "auto attendant," "Background," "WaitExten," "Read()," "GotoIfTime," "business hours," "invalid extension," "timeout extension," "prompt recording," "sln," "asterisk sounds," or "press 1 for." Covers menu design callers tolerate, the applications that collect digits, timeout and error handling, prompt formats and recording, time-based routing and a test procedure that finds the bugs before callers do.