An open-source, HIPAA-eligible Twilio alternative

Published: (January 6, 2026 at 03:46 PM EST)
6 min read
Source: Dev.to

Source: Dev.to
Note: For the most up‑to‑date setup instructions, see open-telephony-stack/README.md in the repository.

Why we built this

Last summer we were building AI voice agents for healthcare practices. We needed to:

  • Make and receive calls
  • Stream audio in real‑time
  • Remain HIPAA‑eligible

Twilio looked like the obvious choice—until we hit the paywall: $2,000 /month for a Business Associate Agreement (BAA) before a single call was placed. For a startup, that price is prohibitive.

So we created our own stack:

ComponentWhat it does
AsteriskOpen‑source PBX (Dockerized)
AWS Chime SDKSIP trunking & phone numbers
FastAPI shimBridges legacy telephony to modern WebSocket APIs

The result is a complete, secure telephony system that can handle inbound and outbound calls:

  • Receives calls via AWS Chime Voice Connector (real PSTN number)
  • Terminates SIP/TLS on Asterisk (Docker)
  • Bridges audio via RTP to a WebSocket connection
  • Streams base64 μ‑law audio to your AI voice server
  • Exposes a Twilio‑like WebSocket API (modeled after Twilio Media Streams)

You bring your own AI; the stack only handles the phone infrastructure.

Use‑case examples

ScenarioWhy this stack helps
Healthcare AI – need HIPAA compliance without Twilio’s BAA costsNo extra compliance fees; you control the data
Custom call handling – Twilio limits youFull control over dialplan, media routing, etc.
Full stack ownership – want to own every layerSelf‑hosted, open‑source, no vendor lock‑in
Learning/experimenting – understand telephony internalsEnd‑to‑end, from PSTN to WebSocket, all in code

Consider alternatives if:

  • You only need basic voice for a side project (Twilio is easier).
  • You don’t want to manage infrastructure.
  • You have no special compliance requirements.

Trade‑off: Managing this stack requires time and ongoing maintenance.

Service ports

ServicePortProtocolDescription
Asterisk SIP5061TCP/TLSSIP signaling with AWS Chime
Asterisk ARI8088HTTPAsterisk REST Interface (localhost only)
Shim server8080HTTPFastAPI server, health endpoints
RTP media10000‑10299UDPAudio streams to/from Asterisk

Architecture overview

1. AWS Chime Voice Connector

  • PSTN gateway. You provision a phone number here.
  • Calls arrive as SIP/TLS on port 5061.

2. Asterisk PBX (Docker)

  • Handles SIP signaling, RTP media, call routing.
  • Uses ARI (Asterisk REST Interface) instead of traditional dialplan scripting.

3. Shim server (FastAPI)

FunctionDetails
Connect to Asterisk via ARI WebSocketReceives StasisStart events
Create ExternalMedia channelsBridges RTP to your AI voice server
Maintain 20 ms RTP cadenceInsulates WebSocket jitter
Forward audioBase64 μ‑law payloads to downstream voice server

4. Your AI voice server

  • Any server that can speak the Twilio‑compatible WebSocket media format (e.g., OpenAI Realtime, AWS Nova Sonic, custom ASR/TTS).
  • Sample implementation: open-telephony-stack/src/servers/voice_agent_server.py.

DNS & TLS setup

DNS record (required before TLS)

Record typeNameValueTTL
Asip.yourdomain.comYour Elastic IP (e.g., 54.123.45.67)300 (or default)

Create this A record before:

  • Requesting Let’s Encrypt certificates (Certbot validates domain ownership)
  • Configuring AWS Chime Voice Connector termination (Chime must resolve the hostname)
  • Setting external_signaling_address in pjsip.conf (must match the DNS name)

After adding the record, wait for propagation (a few minutes to 48 hours). Verify with:

dig sip.yourdomain.com
# or
nslookup sip.yourdomain.com

TLS with Let’s Encrypt

  • Certbot runs on the EC2 instance, bound to port 80.
  • Certificates are issued for sip.yourdomain.com.
  • Asterisk reads the certs from /etc/letsencrypt/live/... via a Docker volume mount.
  • A renewal hook reloads Asterisk when certificates rotate.
  • Chime validates the cert against Let’s Encrypt’s CA root – no self‑signed certs, no manual renewal, no surprise expirations.

Call flow (what happens when someone dials your number)

  1. Caller dials your AWS Chime phone number.

  2. Chime sends a SIP INVITE to your Asterisk server (TLS:5061).

  3. Asterisk matches the call in extensions.conf

    Answer()
    Stasis(voice-agent)
  4. ARI sends a StasisStart event to the shim server via WebSocket.

  5. Shim server performs the following steps:

    a. Opens a WebSocket to your voice server.
    b. Creates an ARI mixing bridge.
    c. Adds the PSTN channel to the bridge.
    d. Allocates a UDP port for RTP (10000‑10299; each live call gets its own port).
    e. Creates an ExternalMedia channel pointing to that port.
    f. Adds the ExternalMedia channel to the bridge.

  6. Audio flow:

    PSTN ↔ Bridge ↔ ExternalMedia ↔ Shim (RTP) ↔ Voice Server (WSS)
  7. Call termination (caller hangs up or AI ends the call via an ARI tool call):

    • ARI sends ChannelHangupRequest / ChannelDestroyed.
    • Shim cleans up: closes WebSocket, deletes bridge, releases the RTP port.

Configuration files

All config files live under deployment/asterisk-server/asterisk-config/. The Docker container mounts this directory.

pjsip.conf – SIP trunk configuration

This is the most important file. It defines the SIP trunk to AWS Chime, including transport settings, TLS certificates, inbound/outbound endpoints, and the external_signing_address that must match the DNS name you created.

(The rest of the repository contains additional config files, Docker Compose files, and example scripts.)

AWS Chime SDK + Asterisk Shim – Quick‑Start Guide

Below is a cleaned‑up version of the original markdown. All headings, code blocks, tables and bullet points have been formatted for readability while preserving the original content.


Overview

FilePurpose
pjsip.confSIP transport, TLS settings, and Chime Voice Connector host.
extensions.confMinimal dialplan – routes calls into the ARI Stasis application (voice‑agent).
ari.confCredentials for the Asterisk REST Interface (ARI).
http.confBuilt‑in HTTP server for ARI (bound to localhost for security).
rtp.confUDP port range for RTP media streams (default 10000‑10299).
modules.confLoads only the needed modules: PJSIP, ARI, and the μ‑law codec.

Notes

  • external_signaling_address must match your DNS name and the TLS certificate.
  • local_net tells Asterisk what is “inside” vs. “outside” for NAT handling.
  • verify_server=no because Chime does not present a client certificate.
  • The cert/key files are what Asterisk presents to Chime during the TLS handshake.

Prerequisites

  • AWS account
  • EC2 instance (recommended t3.medium or larger, Amazon Linux 2023)
  • Elastic IP attached to the EC2 VM (Chime Voice Connectors require a static IP)
  • Domain name with an A record pointing to the Elastic IP
  • Docker & Docker Compose installed on the instance

Configure a Chime Voice Connector

  1. Open the AWS Chime SDK console.
  2. Create a Voice Connector (or edit an existing one).
SettingValue
Hostsip.yourdomain.com
Port5061
ProtocolTLS
  1. Note the Voice Connector hostname – you’ll need it for pjsip.conf.

Obtain a TLS Certificate (Let’s Encrypt)

# Install certbot
sudo yum install -y certbot

# Request a certificate (port 80 must be open)
sudo certbot certonly --standalone \
  --preferred-challenges http \
  -d sip.yourdomain.com \
  --agree-tos -m your@email.com

# Enable automatic renewal
sudo systemctl enable --now certbot-renew.timer

# Create a renewal hook that reloads Asterisk inside Docker
sudo mkdir -p /etc/letsencrypt/renewal-hooks/deploy
sudo tee /etc/letsencrypt/renewal-hooks/deploy/reload-asterisk.sh > /dev/null

The repository also ships a Lambda function that automatically updates the security group whenever AWS publishes new IP ranges for the services AMAZON, EC2, and CHIME_VOICECONNECTOR.

Deploy the Asterisk Server (Docker)

# Change to the Docker deployment directory
cd deployment/asterisk-server

# -------------------------------------------------
# Edit the configuration files as needed:
#   - pjsip.conf : domain, cert paths, voice‑connector host
#   - ari.conf   : secure ARI username/password
#   - rtp.conf   : adjust port range if required
# -------------------------------------------------

# Start the Asterisk container
docker-compose up -d

# Follow the logs
docker logs -f asterisk-server

# Open an interactive Asterisk CLI
docker exec -it asterisk-server asterisk -rvvvvv

Prepare the Shim Server

Create an .env file

cat > .env <<'EOF'
ARI_BASE=http://127.0.0.1:8088/ari
ARI_USER=ariuser
ARI_PASS=your-secure-password-here
ARI_APP=voice-agent
EXTERNAL_MEDIA_HOST=127.0.0.1
ECS_MEDIA_WSS_URL=wss://your-voice-server.internal/voice/voice
RTP_PORT_START=10000
RTP_PORT_END=10299
EOF

Build & Run the Shim

# Build the shim Docker image
docker build -t asterisk-shim -f deployment/shim-server/Dockerfile .

# Run the shim (host network so it can bind to the RTP ports)
docker run -d --env-file .env --network host --name asterisk-shim asterisk-shim

# Verify the shim health endpoint
curl http://localhost:8080/health

Test the End‑to‑End Flow

  1. Call your AWS Chime phone number (the number assigned to the Voice Connector).

  2. Watch the logs:

    # Asterisk logs (SIP/RTP activity)
    docker logs -f asterisk-server
    
    # Shim server logs (session lifecycle)
    docker logs -f asterisk-shim

    You should see entries similar to:

    INVITE received
    CallSession created
    ExternalMedia channel established

WebSocket API (Shim ↔ Voice Server)

The API mirrors Twilio Media Streams – same event structure and μ‑law audio format.

Audio Format

PropertyValue
Codecμ‑law (PCMU)
Sample rate8000 Hz
Frame size160 bytes (20 ms)
EncodingBase64

Event Payloads

start (shim → voice server)

{
  "event": "start",
  "start": {
    "streamSid": "unique-stream-id",
    "callSid": "asterisk-channel-id",
    "customParameters": {
      "source": "asterisk-shim",
      "format": "ulaw"
    }
  }
}

media (bidirectional)

{
  "event": "media",
  "streamSid": "unique-stream-id",
  "media": {
    "payload": "base64-encoded-ulaw-audio",
    "timestamp": 1234
  }
}

clear (voice server → shim)

{ "event": "clear" }

mark (bidirectional)

{
  "event": "mark",
  "streamSid": "unique-stream-id",
  "mark": { "name": "responsePart" }
}

stop (either direction)

{
  "event": "stop",
  "streamSid": "unique-stream-id"
}

Sample Implementation

A minimal example, voice_agent_server.py, is included in the repository. It demonstrates:

  • Handling the WebSocket events above
  • Real‑time audio processing
Back to Blog

Related posts

Read more »

How AWS re:Invented the cloud

From the floor at AWS re:Invent, Ryan is joined by AWS Senior Principal Engineer David Yanacek to chat about all things AWS, from the truth behind AWS’s Black F...

Level 0 of my DevOps journey

Level 0 DevOps Summary: What I’ve Done 1. Created and Connected to an EC2 Instance - Launched an AWS EC2 instance Linux‑based. - Connected using WSL on the loc...

What was your win this week??

markdown !Forem Logohttps://media2.dev.to/dynamic/image/width=65,height=,fit=scale-down,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2...