___ ____ ____ ____ __ __ __ ____ ____
( _)( _ \ / ___)( _ \( \/ ) /__\ ( _ \(_ _)
)(_ )___/( (__ )(_) )) ( _ /(__)\ ) / _)(_
(___)(__) \___) (____/(_/\/\_)(__) (__)(__)(_)\_)(____)
A modern TypeScript client for Asterisk REST Interface
- Complete Asterisk ARI support
- Written in TypeScript with full type support
- Fully typed event system with autocomplete (
AriEventMap) - Listener dedup — safe to register the same listener twice
- WebSocket support for real-time events
- Automatic reconnection management
- Simplified channel and playback handling
- ESM and CommonJS support
- Unit tested with Vitest
npm install @ipcom/asterisk-ariimport { AriClient } from '@ipcom/asterisk-ari';
const client = new AriClient({
host: 'localhost', // Asterisk host
port: 8088, // ARI port
username: 'username', // ARI username
password: 'password', // ARI password
secure: false // Use true for HTTPS/WSS
});// Connect to WebSocket to receive events
await client.connectWebSocket(['myApp']); // 'myApp' is your application name
// Listen for specific events
client.on('StasisStart', (event) => {
console.log('New channel started:', event.channel.id);
});
client.on('StasisEnd', (event) => {
console.log('Channel ended:', event.channel.id);
});
// Listen for DTMF events
client.on('ChannelDtmfReceived', (event) => {
console.log('DTMF received:', event.digit);
});
// Close WebSocket connection
client.closeWebSocket();When working with WebSocket events, you get access to both the raw event data and convenient instance objects that allow direct interaction with the channel or playback:
client.on('StasisStart', async (event) => {
// event.channel contains the raw channel data
console.log('New channel started:', event.channel.id);
// event.instanceChannel provides a ready-to-use ChannelInstance
const channelInstance = event.instanceChannel;
// You can directly interact with the channel through the instance
await channelInstance.answer();
await channelInstance.play({ media: 'sound:welcome' });
});
client.on('BridgeCreated', async (event) => {
// event.bridge contains the raw bridge data
console.log('Bridge created:', event.bridge.id);
// event.instanceBridge provides a ready-to-use BridgeInstance
const bridgeInstance = event.instanceBridge;
// Direct control through the instance
await bridgeInstance.add({ channel: ['channel-id-1', 'channel-id-2'] });
});
// Similarly for playback events
client.on('PlaybackStarted', async (event) => {
// event.playback contains the raw playback data
console.log('Playback ID:', event.playback.id);
// event.instancePlayback provides a ready-to-use PlaybackInstance
const playbackInstance = event.instancePlayback;
// Direct control through the instance
await playbackInstance.control('pause');
});This approach provides two key benefits:
- No need to manually create instances using
client.Channel()orclient.Playback() - Direct access to control methods without additional setup
Traditional approach:
client.on('StasisStart', async (event) => {
// Need to create channel instance manually
const channel = client.Channel(event.channel.id);
await channel.answer();
});Using instance from event:
client.on('StasisStart', async (event) => {
// Instance is already available
await event.instanceChannel.answer();
});This feature is particularly useful when handling multiple events and needing to perform actions on channels or playbacks immediately within event handlers.
// Create a channel instance
const channel = client.Channel();
// Originate a call
await channel.originate({
endpoint: 'PJSIP/1000',
extension: '1001',
context: 'default',
priority: 1
});
// Answer a call
await channel.answer();
// Play audio
const playback = await channel.play({
media: 'sound:welcome'
});
// Hangup the channel
await channel.hangup();// Create a playback instance
const playback = client.Playback();
// Monitor playback events
playback.on('PlaybackStarted', (event) => {
console.log('Playback started:', event.playback.id);
});
playback.on('PlaybackFinished', (event) => {
console.log('Playback finished:', event.playback.id);
});
// Control playback
await playback.control('pause'); // Pause
await playback.control('unpause'); // Resume
await playback.control('restart'); // Restart
await playback.stop(); // Stop// Create a bridge instance
const bridge = client.Bridge();
// Create a new bridge with specific settings
await bridge.getDetails();
// Add channels to the bridge
await bridge.add({
channel: ['channel-id-1', 'channel-id-2']
});
// Remove channels from the bridge
await bridge.remove({
channel: ['channel-id-1']
});
// Play audio on the bridge
const playback = await bridge.playMedia({
media: 'sound:announcement',
lang: 'en'
});
// Stop playback on the bridge
await bridge.stopPlayback(playback.id);
// Set video source
await bridge.setVideoSource('video-channel-id');
// Clear video source
await bridge.clearVideoSource();// Create an instance for a specific channel
const channel = client.Channel('channel-id');
// Monitor specific channel events
channel.on('ChannelStateChange', (event) => {
console.log('Channel state changed:', event.channel.state);
});
channel.on('ChannelDtmfReceived', (event) => {
console.log('DTMF received on channel:', event.digit);
});
// Get channel details
const details = await channel.getDetails();
console.log('Channel details:', details);
// Handle channel variables
await channel.getVariable('CALLERID');
await channel.setVariable('CUSTOM_VAR', 'value');// Play audio on a specific channel
const channel = client.Channel('channel-id');
const playback = await channel.play({
media: 'sound:welcome',
lang: 'en'
});
// Monitor specific playback
playback.on('PlaybackStarted', (event) => {
console.log('Playback started on channel');
});
// Control playback
await channel.stopPlayback(playback.id);
await channel.pausePlayback(playback.id);
await channel.resumePlayback(playback.id);// Create an instance for a specific bridge
const bridge = client.Bridge('bridge-id');
// Monitor bridge events
bridge.on('BridgeCreated', (event) => {
console.log('Bridge created:', event.bridge.id);
});
bridge.on('BridgeDestroyed', (event) => {
console.log('Bridge destroyed:', event.bridge.id);
});
bridge.on('BridgeMerged', (event) => {
console.log('Bridge merged:', event.bridge.id);
});
// Get bridge details
const details = await bridge.get();
console.log('Bridge details:', details);
// Monitor channel events in bridge
bridge.on('ChannelEnteredBridge', (event) => {
console.log('Channel entered bridge:', event.channel.id);
});
bridge.on('ChannelLeftBridge', (event) => {
console.log('Channel left bridge:', event.channel.id);
});try {
await client.connectWebSocket(['myApp']);
} catch (error) {
console.error('Error connecting to WebSocket:', error);
}
// Using with async/await
try {
const channel = client.Channel();
await channel.originate({
endpoint: 'PJSIP/1000'
});
} catch (error) {
console.error('Error originating call:', error);
}Every event is mapped in the AriEventMap interface, so client.on() gives you full autocomplete and type inference:
// The event parameter is automatically typed — no manual annotations needed
client.on('StasisStart', (event) => {
event.channel.id; // string ✔
event.args; // string[] ✔
event.instanceChannel; // ChannelInstance | undefined ✔
});
client.on('ChannelDtmfReceived', (event) => {
event.digit; // string ✔
event.duration_ms; // number ✔
});You can also import AriEventMap and individual event types directly:
import type { AriEventMap, StasisStart, ChannelDtmfReceived } from '@ipcom/asterisk-ari';
function handleStart(event: StasisStart) {
console.log(event.channel.name);
}
// Or use a lookup on the map
type MyEvent = AriEventMap['PlaybackFinished'];AriEventMap is an interface, so you can extend it via declaration merging to add your own events with full type safety:
declare module '@ipcom/asterisk-ari' {
interface AriEventMap {
MyCustomEvent: {
type: 'MyCustomEvent';
payload: { foo: string };
application: string;
};
}
}
// Now fully typed — autocomplete works for 'MyCustomEvent'
client.on('MyCustomEvent', (event) => {
console.log(event.payload.foo); // string ✔
});The project uses Vitest for unit testing.
# Run all tests
npm test
# Run in watch mode
npm run test:watchThe library provides access to many other ARI features:
- Endpoint handling
- Sound manipulation
- Application control
- Asterisk system information
- And much more...
// Create and manage a bridge
const bridge = await client.bridges.createBridge({
type: 'mixing',
name: 'myBridge'
});
// Add channels to bridge
await client.bridges.addChannels(bridge.id, {
channel: ['channel-id-1', 'channel-id-2']
});// Start recording on a channel
const channel = client.Channel('channel-id');
await channel.record({
name: 'recording-name',
format: 'wav',
maxDurationSeconds: 60,
beep: true
});// Create external media channel
const channel = await client.channels.createExternalMedia({
app: 'myApp',
external_host: 'media-server:8088',
format: 'slin16'
});For complete API documentation, please refer to the TypeScript types and interfaces exported by the package.
Contributions are welcome! Please feel free to submit a Pull Request.
Apache-2.0