Telephony with Asterisk and SIP

This content was generated with the assistance of AI and should be verified against the official documentation of each tool and framework before being relied on in production.

This section’s bibliography lists the reference material consulted while preparing these pages.

Written on 2026-10-01 against Asterisk 23.5.0, 22.11.0 and 20.21.0 (a 24.0.0 release candidate exists; see the version baseline), the chan_websocket channel driver (available from Asterisk 23.0.0, 22.6.0, 21.11.0 and 20.16.0), the third-party FreeSWITCH module mod_audio_stream 1.1.0 (MIT), Jambonz, the OpenAI Realtime SIP integration, Python 3.12 with Pipecat 1.12.0 and FastAPI 0.142.2, and Spring Boot 4.1.x on Java 21. Asterisk, a SIP trunk and a telephone cannot be exercised from this repository, so every step on this page was documented from the official pages linked after each block and none was run: the configuration files, dialplan, ARI calls and service code are illustrative, not compiled, and the exact option names must be checked against the documentation of the Asterisk version you install. This is the "company PBX" path of the running scenario: a colleague dials extension 7000 and reaches the DocsAssistant.

Topology of the company PBX path. A caller on the public telephone network reaches an ITSP SIP trunk, which sends SIP over TLS with SRTP media through an edge firewall or session border controller to Asterisk. Asterisk routes extension 7000 to a Stasis application. A voice service written with Pipecat in Python or Spring Boot in Java controls Asterisk through ARI and receives the call audio over chan_websocket, AudioSocket or RTP. The voice service calls speech to text, the DocsAssistant core with retrieval and text to speech. Alternatives that skip the PBX audio hop are a SIP trunk straight to OpenAI Realtime, ElevenLabs Agents or LiveKit SIP, or FreeSWITCH and Jambonz instead of Asterisk.

SIP trunking basics

SIP (RFC 3261) sets up, changes and ends sessions; the audio itself travels separately as RTP (RFC 3550), described in the session by SDP (RFC 8866). Four terms cover most of what this page needs:

  • SIP trunk. A logical link between your PBX and a provider (an ITSP) that carries many calls and the telephone numbers (DIDs) you rent from it. Authentication is by registration with credentials or by the provider’s source IP address.

  • Dialog and signalling. An INVITE creates the call, the provider answers 100, 180 and 200, and BYE ends it.

  • Media. The 200 OK carries an SDP answer that names the codec and the RTP address and port. Signalling and media can take different network paths, which is why NAT and firewalls cause one-way or no audio.

  • Codec negotiation. The PBX offers a list and the far end picks; Telephone networks mostly use G.711 mu-law or A-law at 8 kHz, and wideband codecs such as G.722 or Opus appear only between modern endpoints.

For a voice agent the trunk is only a way in. The agent must answer in under about a second after the caller stops speaking (see Voice Architectures for the budget) and the PBX adds a media hop, so keep Asterisk and the voice service in the same data centre.

Asterisk building blocks

Asterisk models every leg of a call as a channel. Channels join bridges, and a dialplan (extensions.conf) decides what runs when a call arrives. The pieces used here:

Piece Role for the voice agent

PJSIP (pjsip.conf)

The SIP stack. Defines the trunk endpoint, its authentication and its address of record.

Dialplan (extensions.conf)

Routes the DID or extension 7000 to the agent with Stasis(), AudioSocket() or Dial(WebSocket/…​).

ARI (ari.conf, http.conf)

REST interface plus an events WebSocket. A Stasis application takes control of a channel: your code decides to answer, create bridges, add channels and hang up.

External Media

An ARI operation that creates a channel whose audio goes to an external host instead of a phone.

AudioSocket

A small TCP protocol that carries 16-bit signed linear PCM frames, with a UUID that identifies the call.

chan_websocket

A channel driver that carries audio as WebSocket binary frames and control as text frames, in either direction of connection.

A minimal trunk and dialplan

The sketch below registers to a provider and sends calls for extension 7000 and for the rented DID to a Stasis application. Names, hosts and secrets are placeholders.

pjsip.conf: outbound registration trunk (excerpt)
[transport-tls]
type=transport
protocol=tls
bind=0.0.0.0:5061
cert_file=/etc/asterisk/keys/asterisk.crt
priv_key_file=/etc/asterisk/keys/asterisk.key
method=tlsv1_2

[itsp]
type=registration
outbound_auth=itsp
server_uri=sip:sip.example-itsp.test
client_uri=sip:company-account@sip.example-itsp.test
transport=transport-tls

[itsp]
type=auth
auth_type=userpass
username=company-account
password=CHANGE_ME

[itsp]
type=aor
contact=sip:sip.example-itsp.test

[itsp]
type=endpoint
transport=transport-tls
context=from-trunk
disallow=all
allow=ulaw,alaw
outbound_auth=itsp
aors=itsp
media_encryption=sdes

[itsp]
type=identify
endpoint=itsp
match=203.0.113.10
extensions.conf: send extension 7000 to a Stasis application
[from-trunk]
exten => _+X.,1,Goto(internal,7000,1)

[internal]
exten => 7000,1,NoOp(DocsAssistant call from ${CALLERID(num)})
 same => n,Stasis(docs-assistant,${EXTEN})
 same => n,Hangup()

Goto() with only an extension and a priority stays in the current context, so the jump out of [from-trunk] names the internal context explicitly. The same-named sections in pjsip.conf are legal because each has a different type. The endpoint sets media_encryption=sdes so that SRTP is negotiated; the security section below explains why. Registration trunks need an outbound_auth on the endpoint, while IP-authenticated trunks need only the identify section.

ARI: Stasis, bridges and External Media

The Asterisk REST Interface lets an external program drive a call. Enable the HTTP server and an ARI user, then connect an events WebSocket for the application name used in Stasis().

http.conf and ari.conf
; http.conf
[general]
enabled=yes
bindaddr=127.0.0.1
bindport=8088

; ari.conf
[general]
enabled=yes

[docs-voice]
type=user
read_only=no
password=CHANGE_ME
password_format=plain

When the dialplan runs Stasis(docs-assistant,7000) the application receives a StasisStart event with the caller channel. The application then answers, creates a mixing bridge, adds the caller to it, creates an External Media channel and adds that to the same bridge, so audio flows caller to voice service and back. The External Media operation takes these parameters (from the Channels REST API reference):

Parameter Values Meaning

app, external_host, format

Stasis app name, host:port, codec such as ulaw, slin16

Required. For the websocket client mode external_host names a websocket_client.conf entry; INCOMING is used with connection_type=server.

encapsulation

rtp (default), audiosocket, none

Payload wrapper. Must be none for the websocket transport.

transport

udp (default), tcp, websocket

How the media is carried: UDP for RTP, TCP for AudioSocket, websocket for chan_websocket.

connection_type

client (default), server

server is valid only for the websocket transport: your service connects to Asterisk.

direction

both

Only both is supported.

transport_data

optional

For websocket, appended to the dialstring, for example f(json) for JSON control messages.

The three transports differ in what your service must implement:

  • RTP (encapsulation=rtp, transport=udp): you receive plain RTP packets at external_host, and Asterisk sets the UNICASTRTP_LOCAL_ADDRESS and UNICASTRTP_LOCAL_PORT variables on the new channel so that you know where to send audio back. You write an RTP depacketiser, with sequence numbers, timestamps and jitter handling, yourself.

  • AudioSocket (encapsulation=audiosocket, transport=tcp): a TCP stream of frames with a three-byte header, one byte of type and two bytes of big-endian length. Type 0x01 is the call UUID, 0x10 is 16-bit little-endian PCM at 8 kHz (0x12 is 16 kHz), 0x03 is a DTMF digit, 0x00 terminates and 0xff is an error. There is no RTP to parse.

  • WebSocket (transport=websocket, encapsulation=none): binary frames carry audio and text frames carry control, handled in the next section.

AudioSocket can also be used straight from the dialplan, without ARI, which is the simplest possible route:

extensions.conf: AudioSocket from the dialplan
exten => 7001,1,Answer()
 same => n,Set(CALL_UUID=${SHELL(uuidgen | tr -d '\n')})
 same => n,AudioSocket(${CALL_UUID},voice.internal:9019)
 same => n,Hangup()

AudioSocket(uuid,service) takes the UUID and the host:port of your TCP server and exchanges 16-bit, 8 kHz mono PCM by default. Generating the UUID with uuidgen through SHELL() is one option; the UUID() dialplan function, if your Asterisk version provides it, avoids spawning a process. Because dialplan-only AudioSocket has no ARI, the application cannot bridge or transfer the call; it can only end it by closing the socket.

chan_websocket

chan_websocket (available from Asterisk 23.0.0, 22.6.0, 21.11.0 and 20.16.0) turns a WebSocket into a channel. It is the transport recommended by the Asterisk documentation for new work because browsers, FastAPI and Spring all speak WebSocket natively. Two connection modes exist:

  • Outgoing (connection_type=client): Asterisk connects to your server using a section of websocket_client.conf. Only per_call_config connections are supported, and the same connection must not be shared between ARI and media.

  • Incoming (connection_type=server, external_host=INCOMING): Asterisk creates the channel and waits. Your service reads the channel variable MEDIA_WEBSOCKET_CONNECTION_ID and connects to Asterisk’s HTTP server with that ephemeral identifier in the URI.

websocket_client.conf and an ARI call for the outgoing mode
; websocket_client.conf
[docs-voice-service]
type=websocket_client
uri=wss://voice.internal:8443/asterisk/media
protocols=media
connection_type=per_call_config
POST /ari/channels/externalMedia?app=docs-assistant&transport=websocket&encapsulation=none
    &external_host=docs-voice-service&format=slin16&transport_data=f(json)

The dialplan form uses the dialstring Dial(WebSocket/<connection_id>/<options>). The options include c(codec) such as c(ulaw), n to prevent auto-answer, f(format) to choose plain-text or json control messages, p for passthrough (required for Opus, Speex and G.729, where your application then owns framing), d(direction) and v(uri_params).

The protocol has these properties:

  • Audio is sent in binary frames; commands and events are text frames. The JSON format is preferred, and plain text is deprecated.

  • On connection the client receives a MEDIA_START message with the connection id, the channel and the codec. It also reports an optimal frame size (160 bytes for 20 ms of mu-law), so write audio in multiples of it, or use START_MEDIA_BUFFERING and STOP_MEDIA_BUFFERING and let Asterisk reframe.

  • Control commands from the client include ANSWER, HANGUP, PAUSE_MEDIA, CONTINUE_MEDIA, FLUSH_MEDIA, MARK_MEDIA, GET_STATUS, REPORT_QUEUE_DRAINED and SET_MEDIA_DIRECTION.

  • The playback queue holds about 1000 frames, roughly 20 seconds of audio; Asterisk sends MEDIA_XOFF above 900 frames and MEDIA_XON below 800, which a streaming TTS sender must honour. The maximum message is 65,500 bytes.

  • FLUSH_MEDIA is the barge-in primitive: when the voice service detects the caller speaking (see Turn-Taking and Interruptions), it sends it to discard the agent’s queued speech immediately.

A Python ARI service with Pipecat

The service has two jobs: handle ARI events (the control plane) and run the audio pipeline (the media plane). The ARI part below uses aiohttp against the REST and events interfaces; it creates the bridge and an incoming-mode WebSocket External Media channel.

Python: ARI control plane for the DocsAssistant
import asyncio
import json
import aiohttp

ARI = "http://asterisk.internal:8088"
AUTH = aiohttp.BasicAuth("docs-voice", "CHANGE_ME")
APP = "docs-assistant"


async def ari_post(session, path, **params):
    async with session.post(f"{ARI}/ari{path}", params=params, auth=AUTH) as r:
        r.raise_for_status()
        return await r.json() if r.content_type == "application/json" else None


media_channels: set[str] = set()   # External Media channels this service created


async def on_stasis_start(session, caller_id: str):
    await ari_post(session, f"/channels/{caller_id}/answer")
    bridge = await ari_post(session, "/bridges", type="mixing")
    media = await ari_post(
        session, "/channels/externalMedia",
        app=APP, transport="websocket", encapsulation="none",
        external_host="INCOMING", connection_type="server",
        format="slin16", transport_data="f(json)",
    )
    media_channels.add(media["id"])
    await ari_post(session, f"/bridges/{bridge['id']}/addChannel", channel=caller_id)
    await ari_post(session, f"/bridges/{bridge['id']}/addChannel", channel=media["id"])
    # The incoming connection id is a channel variable of the media channel.
    async with session.get(
        f"{ARI}/ari/channels/{media['id']}/variable",
        params={"variable": "MEDIA_WEBSOCKET_CONNECTION_ID"}, auth=AUTH,
    ) as r:
        connection_id = (await r.json())["value"]
    asyncio.create_task(run_pipeline(connection_id))


async def main():
    async with aiohttp.ClientSession() as session:
        url = f"{ARI}/ari/events?app={APP}"
        async with session.ws_connect(url, auth=AUTH) as ws:
            async for msg in ws:
                event = json.loads(msg.data)
                channel_id = event.get("channel", {}).get("id")
                if event["type"] == "StasisStart":
                    # The External Media channel joins the same Stasis app and raises its own StasisStart:
                    # only caller channels start a new bridge, or every media channel would spawn another one.
                    if channel_id not in media_channels:
                        asyncio.create_task(on_stasis_start(session, channel_id))
                elif event["type"] == "StasisEnd":
                    media_channels.discard(channel_id)


asyncio.run(main())

The External Media channel created by the service is itself placed in the docs-assistant Stasis application, so the events socket also delivers a StasisStart for it. The loop therefore remembers the media channel ids it created and starts a bridge only for caller channels; without that check each media channel would trigger another bridge and another media channel. on_stasis_start runs as a task so that one call’s REST round trips do not hold up events for other calls.

run_pipeline(connection_id) is the media plane. In Pipecat it is a pipeline whose transport is a WebSocket client to Asterisk’s HTTP server (incoming mode) and whose frame serializer maps chan_websocket binary audio to InputAudioRawFrame and back (see Pipecat). Pipecat ships serializers for Twilio, Telnyx, Plivo, Exotel and others but not for chan_websocket, so a small FrameSerializer subclass does the mapping: binary frames are raw slin16 audio, text frames are JSON control messages, and an interruption becomes FLUSH_MEDIA.

Python: a chan_websocket serializer and the Pipecat media plane
import json
import os

from pipecat.frames.frames import Frame, InputAudioRawFrame, InterruptionFrame, OutputAudioRawFrame
from pipecat.pipeline.pipeline import Pipeline
from pipecat.pipeline.worker import PipelineParams, PipelineWorker
from pipecat.serializers.base_serializer import FrameSerializer
from pipecat.transports.websocket.client import WebsocketClientParams, WebsocketClientTransport
from pipecat.workers.runner import WorkerRunner

SAMPLE_RATE = 16000   # the channel format is slin16


class AsteriskWebSocketSerializer(FrameSerializer):
    """chan_websocket with f(json): binary frames are audio, text frames are JSON control messages."""

    async def serialize(self, frame: Frame) -> str | bytes | None:
        if isinstance(frame, InterruptionFrame):
            return json.dumps({"command": "FLUSH_MEDIA"})   # barge-in: drop the agent's queued speech
        if isinstance(frame, OutputAudioRawFrame):
            return frame.audio                               # 16-bit little-endian PCM at 16 kHz
        return None

    async def deserialize(self, data: str | bytes) -> Frame | None:
        if isinstance(data, bytes):
            return InputAudioRawFrame(audio=data, sample_rate=SAMPLE_RATE, num_channels=1)
        return None                                          # MEDIA_START, MEDIA_XOFF/XON, ... (log or act on them)


async def run_pipeline(connection_id: str) -> None:
    transport = WebsocketClientTransport(
        uri=f"ws://asterisk.internal:8088/media/{connection_id}",
        params=WebsocketClientParams(
            audio_in_enabled=True, audio_in_sample_rate=SAMPLE_RATE,
            audio_out_enabled=True, audio_out_sample_rate=SAMPLE_RATE,
            add_wav_header=False, serializer=AsteriskWebSocketSerializer(),
        ),
    )
    stt, user_agg, llm, tts, assistant_agg = build_services()   # as on the Pipecat page; llm calls the DocsAssistant core
    pipeline = Pipeline([transport.input(), stt, user_agg, llm, tts, transport.output(), assistant_agg])
    worker = PipelineWorker(pipeline, params=PipelineParams(audio_in_sample_rate=SAMPLE_RATE,
                                                            audio_out_sample_rate=SAMPLE_RATE))
    await WorkerRunner(handle_sigint=False).add_workers(worker)

The class and module names were checked against the Pipecat 1.12.0 source; the media URI path (/media/<connection id> on the Asterisk HTTP server) and the JSON control-message names should be checked against the WebSocket channel driver page of your Asterisk version. build_services() stands for the STT, context aggregators, LLM step and TTS set up on the Pipecat page; the LLM step can be the custom processor there that calls the DocsAssistant core (LangChain create_agent). The pipeline order is the cascaded one from Voice Architectures. With the ulaw channel format, use 8 kHz and convert mu-law in the serializer instead. A production serializer should also honour MEDIA_XOFF/MEDIA_XON flow control.

A Spring Boot WebSocket service

For the Java track the voice service is a Spring WebSocket handler. Asterisk (in outgoing mode) connects to wss://voice.internal:8443/asterisk/media; every binary message is a slice of caller audio and every text message is a JSON event.

Java: Spring WebSocket endpoint that Asterisk chan_websocket connects to
@Configuration
@EnableWebSocket
class AsteriskMediaConfig implements WebSocketConfigurer {

    private final AsteriskMediaHandler handler;

    AsteriskMediaConfig(AsteriskMediaHandler handler) { this.handler = handler; }

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(handler, "/asterisk/media").setAllowedOrigins("*");
    }
}

@Component
class AsteriskMediaHandler extends AbstractWebSocketHandler {

    private final VoiceTurnPipeline pipeline; // VAD, TranscriptionModel, ChatClient, TextToSpeechModel

    AsteriskMediaHandler(VoiceTurnPipeline pipeline) { this.pipeline = pipeline; }

    private final Map<String, WebSocketSession> safeSessions = new ConcurrentHashMap<>();

    @Override
    public void afterConnectionEstablished(WebSocketSession session) {
        // The pipeline sends audio from Reactor threads and FLUSH_MEDIA from the VAD thread: serialise all sends.
        this.safeSessions.put(session.getId(), new ConcurrentWebSocketSessionDecorator(session, 5_000, 512 * 1024));
    }

    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws IOException {
        JsonNode event = new ObjectMapper().readTree(message.getPayload());
        if ("MEDIA_START".equals(event.path("event").asText())) {
            WebSocketSession safe = this.safeSessions.get(session.getId());
            pipeline.start(session.getId(), audio -> sendAudio(safe, audio),
                    () -> sendText(safe, "{\"command\":\"FLUSH_MEDIA\"}"));
        }
    }

    @Override
    protected void handleBinaryMessage(WebSocketSession session, BinaryMessage message) {
        pipeline.onCallerAudio(session.getId(), message.getPayload()); // 16-bit PCM or ulaw frames
    }

    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
        this.safeSessions.remove(session.getId());
        pipeline.stop(session.getId());
    }

    private void sendAudio(WebSocketSession session, ByteBuffer audio) {
        try { session.sendMessage(new BinaryMessage(audio)); } catch (IOException e) { /* drop */ }
    }

    private void sendText(WebSocketSession session, String json) {
        try { session.sendMessage(new TextMessage(json)); } catch (IOException e) { /* drop */ }
    }
}

VoiceTurnPipeline is the hand-written pipeline of Building a Voice Pipeline by Hand: voice activity detection, a TranscriptionModel (or StreamingTranscriptionModel), the Spring AI ChatClient with the RAG advisor, and a StreamingTextToSpeechModel whose output is converted to the channel codec. Spring AI 1.0 called the speech abstraction SpeechModel; 2.0.1 uses the TextToSpeechModel and TranscriptionModel families shown here. A plain WebSocketSession is not safe for concurrent sendMessage calls, which is why the handler wraps it in ConcurrentWebSocketSessionDecorator before handing it to the pipeline. The command text frame shown for barge-in must match the JSON control format documented for your Asterisk version. Blocking calls belong on virtual threads or a bounded executor, not on the WebSocket’s receive thread; see reactive programming for the alternatives.

FreeSWITCH and Jambonz

Asterisk is not the only PBX that can feed an agent:

  • FreeSWITCH mod_audio_stream (third-party, version 1.1.0, MIT) streams a call’s audio over a WebSocket. From the FreeSWITCH console or the dialplan you start it on the channel’s UUID with a URL, a mix type and a sampling rate, and it sends the caller’s audio to your server and can play audio the server sends back. Because it is not part of FreeSWITCH itself, you compile and maintain it, and you must check its README for the exact command syntax of your release.

  • Jambonz is an open-source CPaaS built on FreeSWITCH. Its llm verb hands the call to a speech-to-speech model: you give a vendor, model, auth and llmOptions, plus actionHook (called when the session ends), toolHook (called when the model requests a function) and eventHook (transcripts and events). Your toolHook is where the DocsAssistant core is called, as in the tool-call pattern of Realtime APIs.

Jambonz: the llm verb with OpenAI Realtime (excerpt)
[
  {
    "verb": "llm",
    "vendor": "openai",
    "model": "gpt-realtime",
    "auth": { "apiKey": "sk-..." },
    "actionHook": "/llm-complete",
    "eventHook": "/llm-event",
    "toolHook": "/llm-tool",
    "llmOptions": {
      "session_update": {
        "type": "realtime",
        "instructions": "You are DocsAssistant. Answer from the documentation using the search tool.",
        "tools": [
          {
            "type": "function",
            "name": "search_docs",
            "description": "Search the documentation and return a short spoken answer",
            "parameters": {
              "type": "object",
              "properties": { "question": { "type": "string" } },
              "required": ["question"]
            }
          }
        ]
      }
    }
  }
]

SIP straight to a voice platform

When the voice platform speaks SIP itself, the PBX only needs to forward the call and the middle service disappears.

  • OpenAI Realtime SIP. A SIP trunk (or an Asterisk Dial()) targets sip:$PROJECT_ID@sip.api.openai.com;transport=tls (the EU residency endpoint is sip-eu.api.openai.com). An incoming call triggers a realtime.call.incoming webhook with the call_id and SIP headers. Your server answers with POST /v1/realtime/calls/{call_id}/accept and a session body, and can later reject, refer (transfer to target_uri) or hangup. Media is G.711 mu-law or A-law protected by SRTP. Tools for the DocsAssistant are configured on the session or on a server-side WebSocket, as in Realtime APIs.

  • ElevenLabs Agents. The platform provides SIP trunking numbers and credentials for a trunk to dial; the agent behind it is the one described in ElevenLabs Agents, with the custom LLM pointing at the DocsAssistant core.

  • LiveKit SIP. A LiveKit inbound trunk and dispatch rule put each caller in a room, where a LiveKit agent worker joins it (see LiveKit Agents).

OpenAI Realtime SIP: accept body for a call
{
  "type": "realtime",
  "model": "gpt-realtime-2.1",
  "instructions": "You are DocsAssistant. Keep answers short and speak numbers clearly."
}
extensions.conf: forward extension 7002 to OpenAI Realtime SIP
exten => 7002,1,NoOp(Forward to OpenAI Realtime)
 same => n,Dial(PJSIP/proj_abc123@openai-sip)
 same => n,Hangup()

The openai-sip endpoint is a PJSIP endpoint with transport=transport-tls and media_encryption=sdes whose AOR contact is the OpenAI SIP URI; you define it as in the earlier trunk sketch. proj_abc123 stands for your project id.

Vonage and Telnyx are carriers that can sit on either side. Vonage Voice API can open a WebSocket to your server with the audio of a call (its connect action with a websocket endpoint and an audio/l16;rate=16000 content type). Telnyx offers SIP trunks and a media streaming WebSocket as well, and Pipecat has a Telnyx serializer for it. Treat both as alternatives to the PBX: they hand you the audio over WebSocket, so the voice-service code on this page applies, with their frame formats in place of Asterisk’s. Check each provider’s current documentation for message shapes.

Codecs and transcoding

The codec on the wire is set by negotiation; the codec inside your pipeline is whatever STT and TTS expect. Mismatches cost CPU and quality.

Codec Rate and size Notes

G.711 mu-law (ulaw), A-law (alaw)

8 kHz, 8 bits, 64 kbit/s, 160 bytes per 20 ms

The baseline of the telephone network (mu-law in North America and Japan, A-law elsewhere). Narrowband, so consonants such as "f" and "s" are hard to recognise, and speech models trained on wideband audio lose accuracy.

slin (signed linear 8 kHz), slin16

16-bit PCM at 8 kHz or 16 kHz

Asterisk’s internal format. slin16 doubles the sample rate for models that expect 16 kHz; it adds no information when the caller’s leg is 8 kHz, but avoids a second resample.

G.722

16 kHz wideband

Offered by desk phones and some trunks; improves recognition when end to end.

Opus

8 to 48 kHz, variable

Carried in chan_websocket only in passthrough (p) mode, where your application owns framing and decoding.

G.729

8 kHz, 8 kbit/s

Compressed and licensed; avoid in the agent path, passthrough only.

Transcoding is automatic in Asterisk when two channels share no codec, but each conversion costs CPU and can add a few milliseconds. A practical rule is to request the External Media format that your STT consumes directly: slin16 for a 16 kHz model, ulaw when a vendor accepts G.711. Convert the TTS output back to the channel format once, at the edge of the voice service, and chunk it in multiples of the optimal frame size. The conversion to 8 kHz also means the TTS voice can sound thinner on the phone than in the browser, which is expected rather than a fault.

Security

A PBX connected to the internet is attacked continuously. The agent adds a new cost: every call can invoke paid STT, LLM and TTS services, so abuse is both a telephone bill and a model bill.

  • Encrypt signalling and media. Use SIP over TLS (transport=tls, a valid certificate, TLS 1.2 or later) and SRTP with media_encryption=sdes or DTLS (media_encryption=dtls). OpenAI’s SIP endpoint requires TLS and SRTP; most providers support them. Encrypt the voice-service hop too (wss://), because the audio there is plain PCM.

  • Restrict who can send calls. Allow-list the provider’s IP ranges at the firewall and with PJSIP identify sections; reject calls from unknown sources (an identify match or authentication on every endpoint) and never expose AMI or ARI to the internet. Bind ARI to localhost or an internal network, with a strong password.

  • Prevent toll fraud. Limit outbound dialling to the numbers and countries you need, block premium-rate prefixes, set maximum concurrent calls and per-minute spend alerts on the trunk, and prefer a dialplan that has no outbound route at all for the agent’s context. The agent’s tool set must not include a "transfer to arbitrary number" tool.

  • Limit abuse of the agent. Cap call duration, concurrent agent sessions and silence time, authenticate the voice-service endpoint (mutual TLS or a signed token in v(uri_params)), and throttle by caller id. Rate limiting and similar controls are developed on Production Voice Agents.

  • Protect recordings and transcripts. Call audio is personal data; consent, retention and redaction are covered on the production page. The AI Security & Responsible AI sub-section covers prompt injection in Prompt Injection; its principles apply to spoken input.

Documented only, not tested

Nothing on this page was run. Specifically, these steps are documented from the official pages and unverified here: the PJSIP trunk and TLS/SRTP configuration; the ARI sequence (answer, bridge, External Media, add channels); the websocket_client.conf section and the exact set of chan_websocket control messages and URI for the incoming mode; the AudioSocket dialplan; the Python and Java services (including the unshown Pipecat serializer); the FreeSWITCH mod_audio_stream commands; the Jambonz verb; and the SIP integrations with OpenAI, ElevenLabs and LiveKit. The Asterisk documentation and the asterisk-websocket-examples repository are the sources to follow when you build it.

Choosing a telephony path

Use the Asterisk path when a PBX already exists, calls must stay on your network, or you need on-premises models: ARI with a chan_websocket External Media channel is the recommended starting point on current releases, because it needs no RTP code and supports flushing for barge-in. Choose AudioSocket for the smallest service when you do not need bridges or transfers, and RTP only when another component already speaks it. Choose a direct SIP trunk to OpenAI Realtime, ElevenLabs or LiveKit when you want no media service of your own, and Twilio (Telephony with Twilio) when you also want phone numbers and programmable voice from one vendor. Choose FreeSWITCH or Jambonz when that is already your switch. Whatever the route, keep the DocsAssistant core channel-agnostic and put codec conversion, echo handling and barge-in at the voice-service edge.