ARI Client Library
A Python client library for Asterisk REST Interface (ARI) that provides an object-oriented approach to managing channels, bridges, playbacks, recordings, and events.
Architecture
The library follows a clean architecture pattern with separation of concerns:
- AriClient: Main client class that handles WebSocket connections and event dispatching
- AriClientController: Separate controller class that handles all HTTP API operations
- Model Objects: Bridge, Channel, Playback, and LiveRecording objects that encapsulate state and provide methods for actions
Key Design Principles
- All actions are performed via model objects - Channel, Bridge, Playback, and LiveRecording objects carry their own action methods, keeping operations context-aware and type-safe
- Controller is separate from client - The controller handles HTTP operations, while the client manages WebSocket connections
- Automatic handler injection - When events arrive, all model instances inside them (channels, bridges, playbacks, recordings) are automatically enriched with controller handlers so you can call action methods immediately
Installation
Available on PyPI:
pip install ari-client
# or using uv
uv add ari-client
# or from source
uv sync
Quick Start
import asyncio
from ari_client import (
AriClient,
StasisStartEvent,
StasisEndEvent,
ChannelDtmfReceivedEvent,
ChannelHangupRequestEvent,
PlaybackFinishedEvent,
)
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
client = AriClient(
host="localhost",
port=8088,
ari_user="asterisk",
ari_password="asterisk",
tls_enabled=False
)
@client.on_stasis_start
async def on_stasis_start(event: StasisStartEvent):
logger.info(f"Channel entered Stasis: {event.channel.id}")
await event.channel.answer()
# Play a sound on the channel
playback = await event.channel.play(media="sound:hello-world")
logger.info(f"Playback started: {playback.id}")
# Create a bridge and add the channel
bridge = await client.ari.create_bridge(type="mixing")
await bridge.add_channel(event.channel.id)
@client.on_stasis_end
async def on_stasis_end(event: StasisEndEvent):
logger.info(f"Channel left Stasis: {event.channel.id}")
@client.on_channel_dtmf_received
async def on_dtmf(event: ChannelDtmfReceivedEvent):
logger.info(f"DTMF received: digit={event.digit} on channel {event.channel.id}")
@client.on_playback_finished
async def on_playback_done(event: PlaybackFinishedEvent):
logger.info(f"Playback {event.playback.id} finished with state: {event.playback.state}")
async def main():
await client.connect(app="myapp", subscribe_to_all=True)
try:
await asyncio.sleep(3600)
except KeyboardInterrupt:
logger.info("Shutting down...")
finally:
await client.disconnect()
if __name__ == "__main__":
asyncio.run(main())
Supported Events
All events below can be registered with typed decorator methods or the generic on_event() method.
Stasis
| Event | Decorator | Description |
|---|---|---|
StasisStartEvent |
@client.on_stasis_start |
Channel entered your Stasis application |
StasisEndEvent |
@client.on_stasis_end |
Channel left your Stasis application |
Bridge
| Event | Decorator | Description |
|---|---|---|
BridgeAttendedTransferEvent |
@client.on_bridge_attended_transfer |
Attended transfer on a bridge |
BridgeBlindTransferEvent |
@client.on_bridge_blind_transfer |
Blind transfer on a bridge |
BridgeCreatedEvent |
@client.on_bridge_created |
A bridge was created |
BridgeDestroyedEvent |
@client.on_bridge_destroyed |
A bridge was destroyed |
BridgeMergedEvent |
@client.on_bridge_merged |
Two bridges were merged |
BridgeVideoSourceChangedEvent |
@client.on_bridge_video_source_changed |
Video source changed on a bridge |
Channel
| Event | Decorator | Description |
|---|---|---|
ChannelCallerIdEvent |
@client.on_channel_caller_id |
Caller ID changed on a channel |
ChannelConnectedLineEvent |
@client.on_channel_connected_line |
Connected line information changed |
ChannelCreatedEvent |
@client.on_channel_created |
A channel was created |
ChannelDestroyedEvent |
@client.on_channel_destroyed |
A channel was destroyed |
ChannelDialplanEvent |
@client.on_channel_dialplan |
Channel entered a new dialplan context |
ChannelDtmfReceivedEvent |
@client.on_channel_dtmf_received |
DTMF digit received |
ChannelEnteredBridgeEvent |
@client.on_channel_entered_bridge |
Channel joined a bridge |
ChannelHangupRequestEvent |
@client.on_channel_hangup_request |
Hangup requested on a channel |
ChannelHoldEvent |
@client.on_channel_hold |
Channel placed on hold |
ChannelLeftBridgeEvent |
@client.on_channel_left_bridge |
Channel left a bridge |
ChannelStateChangeEvent |
@client.on_channel_state_change |
Channel state changed |
ChannelTalkingFinishedEvent |
@client.on_channel_talking_finished |
Talking detection stopped |
ChannelTalkingStartedEvent |
@client.on_channel_talking_started |
Talking detection started |
ChannelToneDetectedEvent |
@client.on_channel_tone_detected |
Tone detected on a channel |
ChannelUnholdEvent |
@client.on_channel_unhold |
Channel removed from hold |
ChannelUsereventEvent |
@client.on_channel_userevent |
User-defined event received |
ChannelVarsetEvent |
@client.on_channel_varset |
Channel variable set |
Dial
| Event | Decorator | Description |
|---|---|---|
DialEvent |
@client.on_dial |
Dialing state changed |
Playback
| Event | Decorator | Description |
|---|---|---|
PlaybackContinuingEvent |
@client.on_playback_continuing |
Playback is continuing to the next media URI |
PlaybackFinishedEvent |
@client.on_playback_finished |
Playback completed |
PlaybackStartedEvent |
@client.on_playback_started |
Playback started |
Recording
| Event | Decorator | Description |
|---|---|---|
RecordingFailedEvent |
@client.on_recording_failed |
Recording failed |
RecordingFinishedEvent |
@client.on_recording_finished |
Recording completed |
RecordingStartedEvent |
@client.on_recording_started |
Recording started |
Event Handler Registration
Handlers can be registered in three ways:
# 1. Typed decorator (IDE autocomplete + type checking)
@client.on_channel_destroyed
async def handler(event: ChannelDestroyedEvent):
print(f"Channel {event.channel.id} destroyed: {event.cause_txt}")
# 2. Method call
async def handler(event: ChannelDestroyedEvent):
print(f"Channel {event.channel.id} destroyed: {event.cause_txt}")
client.on_channel_destroyed(handler)
# 3. Generic on_event (works with any event type string or EventType enum)
from ari_client import EventType
@client.on_event(EventType.CHANNEL_DESTROYED)
async def handler(event: ChannelDestroyedEvent):
...
@client.on_event("ChannelDestroyed")
async def handler(event):
...
Model Objects
Channel
Represents an Asterisk channel with action methods.
Properties: id, name, state, caller, connected, accountcode, dialplan, creationtime, language, channelvars
Methods:
| Method | Description |
|---|---|
await channel.answer() |
Answer the channel |
await channel.stop() |
Hang up the channel |
await channel.dial(caller, timeout) |
Dial the channel |
await channel.play(media, lang, ...) |
Play media on the channel, returns Playback |
await channel.record(name, format, ...) |
Record audio, returns LiveRecording |
await channel.snoop(spy, whisper, ...) |
Spy/whisper on the channel, returns Channel |
await channel.send_dtmf(dtmf, ...) |
Send DTMF tones |
await channel.redirect(endpoint) |
Redirect to a different endpoint |
await channel.move(app, app_args) |
Move to another Stasis application |
Bridge
Represents an Asterisk bridge with action methods.
Properties: id, technology, bridge_type, bridge_class, creator, name, channels, video_mode, video_source_id, creationtime
Methods:
| Method | Description |
|---|---|
await bridge.add_channel(channel_id) |
Add a channel to the bridge |
await bridge.stop() |
Destroy the bridge |
await bridge.play(media, lang, ...) |
Play media on the bridge, returns Playback |
await bridge.record(name, format, ...) |
Record bridge audio, returns LiveRecording |
Playback
Represents an active media playback.
Properties: id, media_uri, next_media_uri, target_uri, language, state
Methods:
| Method | Description |
|---|---|
await playback.stop() |
Stop the playback |
await playback.control(operation) |
Control playback: restart, pause, unpause, reverse, forward |
LiveRecording
Represents a live recording in progress.
Properties: name, format, state, target_uri, cause, duration, talking_duration, silence_duration
Methods:
| Method | Description |
|---|---|
await recording.stop() |
Stop recording and store it |
await recording.download() |
Download the stored recording as bytes |
Controller (client.ari)
The AriClientController is accessible via client.ari and provides direct HTTP API access:
Channels: create_channel, answer_channel, stop_channel, originate, originate_with_id, dial, continue_in_dialplan, snoop_channel, send_dtmf, redirect_channel, move_channel, play_channel, record_channel
Bridges: create_bridge, bridge_add_channel, stop_bridge, play_bridge, record_bridge
Playback: stop_playback, control_playback
Recordings: stop_recording, download_recording
External Media: create_external_media
Examples
Call Bridging with External Media
bridge_map: dict[str, Bridge] = {}
@client.on_stasis_start
async def on_stasis_start(event: StasisStartEvent):
if event.channel.name.startswith("UnicastRTP"):
return
await event.channel.answer()
bridge = await client.ari.create_bridge(type="mixing,proxy_media")
await bridge.add_channel(event.channel.id)
external_media = await client.ari.create_external_media(
external_host="192.168.1.100:10000",
format="ulaw"
)
await bridge.add_channel(external_media.id)
await external_media.answer()
bridge_map[event.channel.id] = bridge
@client.on_stasis_end
async def on_stasis_end(event: StasisEndEvent):
bridge = bridge_map.pop(event.channel.id, None)
if bridge:
await bridge.stop()
IVR with DTMF and Playback
from ari_client import (
AriClient, StasisStartEvent,
ChannelDtmfReceivedEvent, PlaybackFinishedEvent,
)
client = AriClient(
host="localhost", port=8088,
ari_user="asterisk", ari_password="asterisk"
)
@client.on_stasis_start
async def on_stasis_start(event: StasisStartEvent):
await event.channel.answer()
await event.channel.play(media="sound:press-1-for-sales")
@client.on_channel_dtmf_received
async def on_dtmf(event: ChannelDtmfReceivedEvent):
if event.digit == "1":
await event.channel.play(media="sound:connecting")
await event.channel.redirect(endpoint="PJSIP/sales")
elif event.digit == "2":
await event.channel.move(app="queue-handler", app_args="support")
elif event.digit == "#":
await event.channel.stop()
@client.on_playback_finished
async def on_playback_done(event: PlaybackFinishedEvent):
logger.info(f"Playback {event.playback.id} done")
Recording a Call
from ari_client import StasisStartEvent, RecordingFinishedEvent
recordings: dict[str, LiveRecording] = {}
@client.on_stasis_start
async def on_stasis_start(event: StasisStartEvent):
await event.channel.answer()
recording = await event.channel.record(
name=f"call-{event.channel.id}",
format="wav",
beep=True,
terminate_on="#",
)
recordings[event.channel.id] = recording
@client.on_recording_finished
async def on_recording_done(event: RecordingFinishedEvent):
logger.info(f"Recording '{event.recording.name}' finished, duration: {event.recording.duration}s")
data = await event.recording.download()
with open(f"/tmp/{event.recording.name}.wav", "wb") as f:
f.write(data)
Bridge Events
from ari_client import (
ChannelEnteredBridgeEvent,
ChannelLeftBridgeEvent,
BridgeDestroyedEvent,
)
@client.on_channel_entered_bridge
async def on_entered(event: ChannelEnteredBridgeEvent):
logger.info(f"Channel {event.channel.id} entered bridge {event.bridge.id}")
@client.on_channel_left_bridge
async def on_left(event: ChannelLeftBridgeEvent):
logger.info(f"Channel {event.channel.id} left bridge {event.bridge.id}")
if len(event.bridge.channels) == 0:
await event.bridge.stop()
@client.on_bridge_destroyed
async def on_bridge_destroyed(event: BridgeDestroyedEvent):
logger.info(f"Bridge {event.bridge.id} destroyed")
Dial Progress Tracking
from ari_client import DialEvent
@client.on_dial
async def on_dial(event: DialEvent):
logger.info(
f"Dial update: {event.peer.name} status={event.dialstatus}"
+ (f" from {event.caller.name}" if event.caller else "")
)
Error Handling
The library includes automatic error handling:
- Event handler exceptions are automatically logged and don't crash the event listener
- HTTP API errors raise exceptions with descriptive messages
- WebSocket connection errors are logged and re-raised
Always wrap your operations in try-except blocks when appropriate:
@client.on_stasis_start
async def on_stasis_start(event: StasisStartEvent):
try:
await event.channel.answer()
except Exception as e:
logger.error(f"Failed to answer channel: {e}")
Metadata
Release files for ari_client 0.8.2
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| ari_client-0.8.2.tar.gz | 26.8 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| ari_client-0.8.2-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 50.8 kB
Release files / ari_client-0.8.2.tar.gz
| Download URL | ari_client-0.8.2.tar.gz |
|---|---|
| Size | 26.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
006434f143ed79b7ebdd1645e6db5dac12f3c5d3dfe0450644135a37b4537f8c
|
|
BLAKE2b-256 checksum How to use checksums |
4d0d387dd1b3292d8f7d77f54dbd321ec016782e7c63913dcfd84ee3ef0f4a7f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.16 {"installer":{"name":"uv","version":"0.12.16","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / ari_client-0.8.2-py3-none-any.whl
| Download URL | ari_client-0.8.2-py3-none-any.whl |
|---|---|
| Size | 23.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
94d14e9663dd42252b77b56dba773cc0884ca24d50237b9a9115d3e5c5edf444
|
|
BLAKE2b-256 checksum How to use checksums |
653cb4216e50f98205b1c6850204c76ff04e9355c1024a14685f7aa56ab686d2
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.16 {"installer":{"name":"uv","version":"0.12.16","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|