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.
use asterisk_rs::ami::{AmiClient,AmiEvent};#[tokio::main]asyncfnmain() -> Result<(),Box<dyn std::error::Error>>{
tracing_subscriber::fmt::init();let client = AmiClient::builder().host("10.0.0.1").credentials("admin","secret").build().await?;// subscribe to hangup events onlyletmut hangups = client.subscribe_filtered(|e| {
e.event_name() == "Hangup"});whileletSome(event) = hangups.recv().await{ifletAmiEvent::Hangup{ channel, cause, cause_txt, .. } = event {
tracing::info!(%channel, %cause, %cause_txt,"channel hung up");}}Ok(())}Use the umbrella crate to pull in whichever protocols you need:
[dependencies]
asterisk-rs = "0.8"Or add individual protocol crates directly:
[dependencies]
asterisk-rs-ami = "0.8"# AMI onlyasterisk-rs-agi = "0.8"# AGI onlyasterisk-rs-ari = "0.8"# ARI onlyThe umbrella crate enables all protocols by default. To select only what you need:
[dependencies]
asterisk-rs = { version = "0.8", default-features = false, features = ["ami"] }
# or: features = ["agi"]# or: features = ["ari"]# or: features = ["ami", "ari"]Available features: ami, agi, ari. The pbx abstraction requires ami.
| 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 |
- Typed actions, events, and commands for the modeled protocol surface; Asterisk 22 is the repository's live-proven boundary
- 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
CompletedCallrecords (channel, duration, cause, full event log) - PBX abstraction --
Pbx::dial()wraps originate + OriginateResponse correlation into one async call - Pending resources -- ARI
PendingChannel/PendingBridgepre-subscribe before REST to eliminate event races - Transport modes -- ARI supports HTTP (request/response) or WebSocket (bidirectional streaming)
- Outbound WebSocket server --
AriServeraccepts 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
use asterisk_rs::ami::AmiClient;#[tokio::main]asyncfnmain() -> 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();whileletSome(call) = rx.recv().await{
tracing::info!(
channel = %call.channel,
duration = ?call.duration,
cause = %call.cause_txt,"call completed");}
tracker.shutdown();Ok(())}use asterisk_rs::agi::{AgiChannel,AgiHandler,AgiRequest,AgiServer};structIvrHandler;implAgiHandlerforIvrHandler{asyncfnhandle(&self,_request:AgiRequest,mutchannel: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]asyncfnmain() -> Result<(),Box<dyn std::error::Error>>{
tracing_subscriber::fmt::init();let(server, _shutdown) = AgiServer::builder().bind("0.0.0.0:4573").allow_external_bind(true).handler(IvrHandler).max_connections(100).build().await?;
server.run().await?;Ok(())}use asterisk_rs::ari::config::AriConfigBuilder;use asterisk_rs::ari::{AriClient,AriEvent,PendingChannel,TransportMode};use asterisk_rs::ari::resources::channel::OriginateParams;#[tokio::main]asyncfnmain() -> 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?;// pre-subscribe before originate so no events are missedlet 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?;whileletSome(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(())}use asterisk_rs::ami::AmiClient;use asterisk_rs::pbx::{DialOptions,Pbx};use std::time::Duration;#[tokio::main]asyncfnmain() -> Result<(),Box<dyn std::error::Error>>{
tracing_subscriber::fmt::init();let client = AmiClient::builder().host("127.0.0.1").credentials("admin","secret").build().await?;letmut 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?;ifletSome(completed) = pbx.next_completed_call().await{
tracing::info!(duration = ?completed.duration, cause = %completed.cause_txt,"call record");}Ok(())}Representative builder, domain-type, and command signatures from these guides are compiled by
just docs-snippets. Exhaustive public API inventories belong to rustdoc.
1.86 or newer.
Licensed under either of
at your option.