asterisk-rs

Async Rust client for Asterisk PBX. Originate calls, handle events, control
channels, bridges, queues, and recordings across all three Asterisk interfaces.
- AMI -- monitor and control Asterisk over TCP. Typed events, actions, automatic reconnection, MD5 auth.
- AGI -- run dialplan logic from your Rust service. FastAGI server with typed async commands.
- ARI -- full call control via REST + WebSocket. Resource handles, typed events with metadata.
Quick Example
use asterisk_rs::ami::{AmiClient, AmiEvent};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
tracing_subscriber::fmt::init();
let client = AmiClient::builder()
.host("10.0.0.1")
.credentials("admin", "secret")
.build()
.await?;
let mut hangups = client.subscribe_filtered(|e| {
e.event_name() == "Hangup"
});
while let Some(event) = hangups.recv().await {
if let AmiEvent::Hangup { channel, cause, cause_txt, .. } = event {
tracing::info!(%channel, %cause, %cause_txt, "channel hung up");
}
}
Ok(())
}
Install
Use the umbrella crate to pull in whichever protocols you need:
[dependencies]
asterisk-rs = "0.1"
Or add individual protocol crates directly:
[dependencies]
asterisk-rs-ami = "0.4"
asterisk-rs-agi = "0.2"
asterisk-rs-ari = "0.4"
Feature Selection
The umbrella crate enables all protocols by default. To select only what you need:
[dependencies]
asterisk-rs = { version = "0.1", default-features = false, features = ["ami"] }
Available features: ami, agi, ari. The pbx abstraction requires ami.
Protocols
| Protocol |
Default Port |
Transport |
Use Case |
| AMI |
5038 |
TCP |
Monitoring, call control, system management |
| AGI |
4573 |
TCP |
Dialplan logic, IVR, call routing |
| ARI |
8088 |
HTTP + WS |
Stasis applications, full media control |
Capabilities
- Typed actions, events, and commands for the full Asterisk protocol surface
- Filtered event subscriptions -- receive only what you need
- Event-collecting actions --
send_collecting() gathers multi-event responses (Status, QueueStatus, etc.)
- Automatic reconnection with exponential backoff, jitter, and re-authentication
- Call tracker -- correlates AMI events into
CompletedCall records (channel, duration, cause, full event log)
- PBX abstraction --
Pbx::dial() wraps originate + OriginateResponse correlation into one async call
- Pending resources -- ARI
PendingChannel/PendingBridge pre-subscribe before REST to eliminate event races
- Transport modes -- ARI supports HTTP (request/response) or WebSocket (bidirectional streaming)
- Outbound WebSocket server --
AriServer accepts Asterisk 22+ outbound WS connections
- Media channel -- low-level audio I/O over WebSocket for external media applications
- Resource handles for ARI (ChannelHandle, BridgeHandle, PlaybackHandle, RecordingHandle)
- Domain types for hangup causes, channel states, device states, dial statuses, and more
- ARI event metadata (application, timestamp, asterisk_id) on every event
- AMI command output capture for
Response: Follows
- URL-safe query encoding, HTTP timeouts, WebSocket lifecycle management
#[non_exhaustive] enums -- new variants won't break your code
- Structured logging via
tracing
More Examples
AMI: call tracker
use asterisk_rs::ami::AmiClient;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
tracing_subscriber::fmt::init();
let client = AmiClient::builder()
.host("127.0.0.1")
.credentials("admin", "secret")
.build()
.await?;
let (tracker, mut rx) = client.call_tracker();
while let Some(call) = rx.recv().await {
tracing::info!(
channel = %call.channel,
duration = ?call.duration,
cause = %call.cause_txt,
"call completed"
);
}
tracker.shutdown();
Ok(())
}
AGI: IVR handler
use asterisk_rs::agi::{AgiChannel, AgiHandler, AgiRequest, AgiServer};
struct IvrHandler;
impl AgiHandler for IvrHandler {
async fn handle(&self, _request: AgiRequest, mut channel: AgiChannel)
-> asterisk_rs::agi::error::Result<()>
{
channel.answer().await?;
channel.stream_file("welcome", "#").await?;
let response = channel.get_data("press-ext", 5000, 4).await?;
tracing::info!(digits = response.result, "caller input");
channel.hangup(None).await?;
Ok(())
}
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
tracing_subscriber::fmt::init();
let (server, _shutdown) = AgiServer::builder()
.bind("0.0.0.0:4573")
.handler(IvrHandler)
.max_connections(100)
.build()
.await?;
server.run().await?;
Ok(())
}
ARI: pending channel
use asterisk_rs::ari::config::AriConfigBuilder;
use asterisk_rs::ari::{AriClient, AriEvent, PendingChannel, TransportMode};
use asterisk_rs::ari::resources::channel::OriginateParams;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
tracing_subscriber::fmt::init();
let config = AriConfigBuilder::new("my-app")
.host("127.0.0.1")
.port(8088)
.username("asterisk")
.password("asterisk")
.build()?;
let client = AriClient::connect(config).await?;
let pending = client.channel();
let params = OriginateParams {
endpoint: "PJSIP/100".into(),
app: Some("my-app".into()),
..Default::default()
};
let (handle, mut events) = pending.originate(params).await?;
while let Some(msg) = events.recv().await {
match msg.event {
AriEvent::StasisStart { .. } => {
handle.answer().await?;
handle.play("sound:hello-world").await?;
handle.hangup(None).await?;
}
AriEvent::ChannelDestroyed { cause_txt, .. } => {
tracing::info!(%cause_txt, "channel destroyed");
break;
}
_ => {}
}
}
Ok(())
}
PBX: dial and wait
use asterisk_rs::ami::AmiClient;
use asterisk_rs::pbx::{DialOptions, Pbx};
use std::time::Duration;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
tracing_subscriber::fmt::init();
let client = AmiClient::builder()
.host("127.0.0.1")
.credentials("admin", "secret")
.build()
.await?;
let mut pbx = Pbx::new(client);
let call = pbx.dial(
"PJSIP/100",
"200",
Some(
DialOptions::new()
.caller_id("Rust PBX <100>")
.timeout_ms(30000),
),
).await?;
call.wait_for_answer(Duration::from_secs(30)).await?;
tracing::info!("call answered");
call.hangup().await?;
if let Some(completed) = pbx.next_completed_call().await {
tracing::info!(duration = ?completed.duration, cause = %completed.cause_txt, "call record");
}
Ok(())
}
Documentation
MSRV
1.83 -- required for async fn in traits (RPITIT).
License
Licensed under either of
at your option.