Getting Started with Asterisk ARI: Build Your First App

Getting Started -- Last reviewed 2026-09-02 ari stasis rest-api python websocket pjsip dialplan getting-started Found this useful? Upvote it. ×

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

When to Use ARI

Use ARI when you need to build a real application around calls, for example:

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:

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:

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.

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:

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.

A solid choice for hosting Asterisk.

High-performance cloud compute starting at $2.50/mo. Deploy a VPS in seconds.

Get $100 Free Credit

Referral link. Helps support this site.

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.

Moderated before publishing. Email never shown.
Related Snippets