Getting Started with Asterisk ARI: Build Your First App
Getting Started with Asterisk ARI: Build Your First App
ARIAsterisk REST Interface. A modern asynchronous API exposing channels, bridges, and endpoints over HTTP and WebSocket for building custom call applications., the Asterisk REST Interface, lets an external program control Asterisk channels, bridges, playbacks, recordings, and other call resources. It is the right tool when your call logic belongs in application code instead of only in extensions.conf.
A working ARI setup has three parts:
| Piece | Purpose |
|---|---|
| HTTP REST API | Your app sends commands such as answer, play audio, create a bridge, add a channelA single call leg passing through Asterisk. Channels represent connections to endpoints and are what dialplan applications act on., or hang up |
| WebSocket events | Asterisk sends your app JSON events such as StasisStart, PlaybackFinished, and StasisEnd |
Stasis() dialplanThe core call-routing configuration of Asterisk, written mostly in extensions.conf as contexts, extensions, and priorities that decide how every call is handled. Full definition → handoff |
The dialplanThe core call-routing configuration of Asterisk, written mostly in extensions.conf as contexts, extensions, and priorities that decide how every call is handled. Full definition → gives a channel to your ARI app so ARI can safely control it |
This guide walks through the first complete workflow: enable HTTP, create an ARI user, send a call into Stasis(), prove the WebSocket and REST API work, then run a minimal Python app that answers, plays hello-world, and hangs up.
On this page
- Getting Started with Asterisk ARI: Build Your First App
- When to Use ARI
- Prerequisites
- Step 1: Enable Asterisk HTTP
- Step 2: Create an ARI User
- Step 3: Add the Stasis Dialplan Handoff
- Step 4: Connect the WebSocket
- Step 5: Send a Test Call into Stasis
- Step 6: Control the Channel with curl
- Step 7: Run a Minimal Python App
- Adding Arguments to Stasis
- Where PJSIP Fits
- Security Checklist
- Troubleshooting
- What to Build Next
- Related References
When to Use ARI
Use ARI when you need to build a real application around calls, for example:
- Click-to-call from a web app
- Custom IVRs driven by a database or API
- Call center workflows that need application state
- Conference, bridge, queueAn Asterisk call queue (app_queue) that parks incoming calls and distributes them to logged-in agents according to a chosen ring strategy., or media control that is awkward in dialplan alone
- Integrations where another system needs to decide what happens next
Do not reach for ARI just because it exists. For ordinary extensionA dialplan entry that matches a dialed number or pattern within a context and triggers a sequence of prioritized steps. routing, ring groups, voicemailAsterisk's built-in voice messaging system (app_voicemail), configured in voicemail.conf with mailboxes per user., queues, and simple IVRs, the dialplan is usually simpler. ARI adds an external process and a network connection. If that process is down, your call flow needs a failure path.
Prerequisites
Before starting, make sure you have:
- Asterisk 12 or newer with
res_ari,res_http_websocket, andapp_stasisavailable - A working PJSIPThe modern SIP channel driver in Asterisk (chan_pjsip), replacing the older chan_sip. Configured in pjsip.conf using endpoints, AORs, auths, and transports. endpointIn PJSIP configuration, the SIP entity Asterisk communicates with: a phone, trunk, or other user agent. Defined by an endpoint section in pjsip.conf., trunkA connection between Asterisk and another PBX or an ITSP/carrier, used to send and receive external calls., or local test channel that can dial into a test contextA named section of the dialplan that groups extensions. Calls enter a specific context and can only reach extensions visible from it, making contexts the basic unit of call routing and security.
- Shell access to the Asterisk host
curlfor REST testingwscatorwebsocatfor WebSocket testing- Python 3.9 or newer if you want to run the minimal app later in this guide (the event handler below uses
str.removeprefix(), added in 3.9)
Install wscat with npm install -g wscat if you have Node.js available. If not, websocat is a standalone binary with no runtime dependency; install it from your distribution's package manager or grab a prebuilt release for your platform.
Useful module checks from the Asterisk CLI:
*CLI> module show like res_ari
*CLI> module show like res_http_websocket
*CLI> module show like app_stasis
If a module is missing, install or rebuild Asterisk with that module enabled before continuing.
Step 1: Enable Asterisk HTTP
ARI uses Asterisk's built-in HTTP server. Edit /etc/asterisk/http.conf:
[general]
enabled = yes
bindaddr = 127.0.0.1
bindport = 8088
Keep ARI on 127.0.0.1 when the ARI application runs on the same host. ARI can control calls, bridges, recordings, and channels, so do not expose it directly to the internet. If the app runs elsewhere, use a private network, firewall rules, an SSH tunnel, or a TLS reverse proxy with strong access controls.
If you changed bindaddr, bindport, or TLS listener settings, restart Asterisk so the socket is rebound:
sudo systemctl restart asterisk
For simple enabled-state changes, a reload may be enough, but a restart is the least ambiguous first-time setup path.
Verify the listener:
asterisk -rx "http show status"
You should see the HTTP server enabled and listening on port 8088.
Step 2: Create an ARI User
Edit /etc/asterisk/ari.conf:
[general]
enabled = yes
pretty = yes
[first-ari-app]
type = user
read_only = no
password = use-a-long-random-password
password_format = plain
Notes:
pretty = yesis helpful while learning because JSON output is easier to read. You can turn it off later.read_only = nois required for an app that answers, plays audio, bridges, or hangs up calls.- Use a unique user for each application. Do not reuse tutorial credentials.
- For production, consider
password_format = cryptinstead of plain text. Generate the hash withopenssl passwd -6 your-passwordand use the output as thepasswordvalue.
Reload ARI and verify the user:
asterisk -rx "module reload res_ari.so"
asterisk -rx "ari show status"
asterisk -rx "ari show users"
ari show status should show ARI enabled. ari show users should list first-ari-app.
Step 3: Add the Stasis Dialplan Handoff
ARI cannot safely control an arbitrary channel that is somewhere else in the dialplan. A channel must enter a named Stasis application first.
Add this to a test context in /etc/asterisk/extensions.conf:
[ari-test]
exten => 7001,1,NoOp(Hand call to first ARI app)
same => n,Stasis(first-ari-app)
same => n,Hangup()
Reload the dialplan:
asterisk -rx "dialplan reload"
asterisk -rx "dialplan show 7001@ari-test"
For a real phone test, route a PJSIP endpoint or inbound DID to 7001@ari-test. For a local-only test, you can originate a Local channel into this extension after the WebSocket is connected.
Stasis(first-ari-app) must match the app parameter your WebSocket client uses. The name is case-sensitive.
Step 4: Connect the WebSocket
Open a terminal and connect to ARI events:
wscat -c 'ws://127.0.0.1:8088/ari/events?app=first-ari-app&api_key=first-ari-app:use-a-long-random-password'
A successful connection registers the Stasis app. Check from another terminal:
asterisk -rx "ari show apps"
asterisk -rx "ari show websocket sessions"
If no WebSocket is connected for first-ari-app, Asterisk will immediately eject channels that enter Stasis(first-ari-app) and return them to the next dialplan priority. That behavior prevents calls from waiting inside an app that is not ready to handle them.
Step 5: Send a Test Call into Stasis
With the WebSocket still connected, originate a local test call:
asterisk -rx "channel originate Local/7001@ari-test application Wait 30"
In the WebSocket terminal, look for a StasisStart event. Copy the channel id from that event. It will look similar to 1753993600.12.
You can also test from a registered PJSIP phone by dialing whatever route sends the call to 7001@ari-test.
Step 6: Control the Channel with curl
Use the channel ID from StasisStart.
Answer the call:
curl -u first-ari-app:use-a-long-random-password \
-X POST "http://127.0.0.1:8088/ari/channels/CHANNEL_ID/answer"
Play the built-in hello-world sound:
curl -u first-ari-app:use-a-long-random-password \
-X POST "http://127.0.0.1:8088/ari/channels/CHANNEL_ID/play?media=sound:hello-world"
Hang up when done:
curl -u first-ari-app:use-a-long-random-password \
-X DELETE "http://127.0.0.1:8088/ari/channels/CHANNEL_ID"
The WebSocket should show events such as StasisStart, ChannelStateChange, PlaybackStarted, PlaybackFinished, and StasisEnd.
If a REST request fails, rerun it with -v so you can see the HTTP status code. A 404 usually means the channel is gone or not controlled by your Stasis app. A 409 usually means the requested operation does not match the channel's current state, for example answering a channel that is already answered.
Step 7: Run a Minimal Python App
Install dependencies in your application environment:
python3 -m venv ari-venv
ari-venv/bin/pip install requests websocket-client
Save this as first_ari_app.py:
import json
import logging
import time
from urllib.parse import urlencode
import requests
import websocket
ARI_HTTP = "http://127.0.0.1:8088/ari"
ARI_WS = "ws://127.0.0.1:8088/ari/events"
APP_NAME = "first-ari-app"
USERNAME = "first-ari-app"
PASSWORD = "use-a-long-random-password"
AUTH = (USERNAME, PASSWORD)
TIMEOUT = 5
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
active_channels = set()
def ari_request(method, path, **kwargs):
url = f"{ARI_HTTP}{path}"
response = requests.request(method, url, auth=AUTH, timeout=TIMEOUT, **kwargs)
if response.status_code in (404, 409):
logging.warning("ARI %s %s returned %s: %s", method, path, response.status_code, response.text)
return None
response.raise_for_status()
if response.text:
return response.json()
return None
def answer_if_needed(channel):
channel_id = channel["id"]
if channel.get("state") != "Up":
ari_request("POST", f"/channels/{channel_id}/answer")
def play_hello(channel_id):
ari_request("POST", f"/channels/{channel_id}/play", params={"media": "sound:hello-world"})
def hangup(channel_id):
ari_request("DELETE", f"/channels/{channel_id}")
def on_message(ws, message):
event = json.loads(message)
event_type = event.get("type")
if event_type == "StasisStart":
channel = event["channel"]
channel_id = channel["id"]
active_channels.add(channel_id)
logging.info("Channel entered Stasis: %s", channel_id)
answer_if_needed(channel)
play_hello(channel_id)
return
if event_type == "PlaybackFinished":
target_uri = event.get("playback", {}).get("target_uri", "")
if target_uri.startswith("channel:"):
channel_id = target_uri.removeprefix("channel:")
if channel_id in active_channels:
logging.info("Playback finished, hanging up: %s", channel_id)
hangup(channel_id)
return
if event_type == "StasisEnd":
channel_id = event["channel"]["id"]
active_channels.discard(channel_id)
logging.info("Channel left Stasis: %s", channel_id)
def on_error(ws, error):
logging.error("WebSocket error: %s", error)
def on_close(ws, status_code, message):
logging.warning("WebSocket closed: %s %s", status_code, message)
def run_forever():
query = urlencode({"app": APP_NAME, "api_key": f"{USERNAME}:{PASSWORD}"})
ws_url = f"{ARI_WS}?{query}"
while True:
logging.info("Connecting to %s", ARI_WS)
ws = websocket.WebSocketApp(
ws_url,
on_message=on_message,
on_error=on_error,
on_close=on_close,
)
ws.run_forever(ping_interval=30, ping_timeout=10)
logging.warning("Disconnected, reconnecting in 2 seconds")
time.sleep(2)
if __name__ == "__main__":
run_forever()
Run it:
ari-venv/bin/python first_ari_app.py
Then send another test call:
asterisk -rx "channel originate Local/7001@ari-test application Wait 30"
The app should log the channel entering Stasis, answer it if needed, play hello-world, hang up, and remove the channel from its active set when StasisEnd arrives.
This is still a learning example. A production app should add structured configuration, better retry policy, request correlation IDs, metrics, graceful shutdown, and tests around event handling.
Adding Arguments to Stasis
Stasis() can pass arguments to your app. This is useful when the dialplan knows the route, tenant, customer, or mode before the app takes over.
[ari-test]
exten => 7002,1,NoOp(Hand call to ARI with arguments)
same => n,Stasis(first-ari-app,support,priority)
same => n,Hangup()
In the StasisStart event, read args:
if event_type == "StasisStart":
args = event.get("args", [])
logging.info("Stasis args: %s", args)
Keep arguments short and non-sensitive. Use your application database for larger context.
Where PJSIP Fits
ARI controls channels after they are in Asterisk. PJSIP configuration still controls how SIPSession Initiation Protocol, the standard signaling protocol used to set up, manage, and tear down VoIP calls between Asterisk and phones or carriers. endpoints register, authenticate, route, and negotiate media.
Typical split:
| Concern | Usually handled by |
|---|---|
| Endpoint registration and auth | pjsip.conf |
| Inbound DID routing into a context | pjsip.conf and dialplan |
| Handing the call to application code | Stasis() |
| Answering, playback, recording, bridge control | ARI |
| SIP header manipulation before outbound INVITE | Dialplan pre-dial handlers or PJSIP header functions |
If your ARI test never receives calls, verify the PJSIP endpoint context first. If your app receives calls but audio or bridging fails, troubleshoot the ARI channel and bridge state next.
Security Checklist
ARI is powerful enough to disrupt live calls, originate fraud traffic, and access call state. Treat it as an administrative interface.
- Bind Asterisk HTTP to localhost unless there is a specific, protected reason not to.
- Use strong unique credentials for each app.
- Use
read_only = yesonly for monitoring clients that should not change call state. - Keep write-capable ARI credentials out of shell history and process managers that expose command lines.
- Use TLS or a trusted private network when ARI traffic leaves the host.
- Restrict access with firewall rules even when TLS is enabled.
- Log ARI application actions with enough context to reconstruct call decisions.
Troubleshooting
| Symptom | What to check |
|---|---|
http show status says disabled |
Confirm [general] enabled = yes in http.conf, then reload or restart Asterisk |
ari show status says disabled |
Confirm [general] enabled = yes in ari.conf, then reload res_ari.so |
| WebSocket returns 401 or closes immediately | Check the api_key=user:password, ari.conf password, and whether special characters need URL encoding |
| WebSocket connects but no events arrive | Confirm the WebSocket app name exactly matches Stasis(first-ari-app) |
Call immediately continues past Stasis() |
No WebSocket is currently registered for that app name, or the app name is wrong |
curl returns 404 for a channel |
The channel ID is wrong, the channel already left Stasis, or your app does not control it |
curl returns 409 when answering |
The channel is already answered or not in a state where answer is valid |
| Playback returns success but no audio is heard | Confirm the sound file exists, the channel is answered, and the endpoint has working RTPReal-time Transport Protocol. Carries the actual audio (media) of a VoIP call after SIP signaling has set it up. |
| Python app exits on Asterisk restart | Add reconnect logic, HTTP timeouts, and exception handling, as shown in the minimal example |
| Local channel tests work but PJSIP calls do not | Check the endpoint context, dialplan route, NAT, codecs, and SIP traces |
Useful commands:
asterisk -rx "http show status"
asterisk -rx "ari show status"
asterisk -rx "ari show users"
asterisk -rx "ari show apps"
asterisk -rx "ari show websocket sessions"
asterisk -rx "core show application Stasis"
asterisk -rx "pjsip show endpoints"
asterisk -rx "pjsip set logger on"
Turn SIP logging off after testing:
asterisk -rx "pjsip set logger off"
What to Build Next
After this first app works, the next ARI concepts are:
- Bridges, for connecting two or more channels
- Playbacks, recordings, and DTMFDual-Tone Multi-Frequency, the touch-tone signals phones send for digit input during a call, used by IVRs and feature codes. collection
- Origination, for starting outbound calls from your app
- External media, for speech analytics or media processing
- Application supervision, so the ARI process starts before calls reach
Stasis()
For a two-party call, the usual pattern is: create a bridge, originate or receive both channels, answer as needed, then add both channels to the bridge.
Related References
- ARI Setup and Stasis Application
- Node.js ARI Client: Channel Events
Stasis()referenceari show statusari show usersari show appsari show websocket sessions- PJSIP Configuration from Scratch
- Configuring PJSIP to Work Through NAT
- PJSIP Authentication
- Working with PJSIP Headers in Asterisk Dialplans
- Debugging Asterisk: SIP Traces and Logging
- Official Asterisk ARI documentation
User Notes
Know a tip or gotcha for this topic? Share it below and help others.
Contribute a note
Share a tip, gotcha, or practical example. Keep it under 2000 characters. No questions (use the Asterisk community forums for support). Wrap code in backticks.