StreamCoreAI SIP Server
August 26, 2026 · View on GitHub
English | 简体中文
A SIP server that bridges SIP phone calls to the StreamCoreAI server. It handles incoming calls (e.g., from a phone network) and outgoing calls (initiated via REST API), converting audio between G.711 μ-law (PCMU) used by SIP and Opus used by StreamCoreAI's WebRTC transport.
For splitting this tree into its own Git repository and publishing github.com/streamcoreai/go-sdk, see Repository layout.
Architecture
Phone Network ←→ SIP (PCMU/RTP) ←→ SIP Server ←→ WHIP (Opus/WebRTC) ←→ StreamCoreAI Server
Each call creates:
- A local RTP listener for SIP-side audio (PCMU at 8kHz)
- A StreamCoreAI SDK client connection via WHIP (Opus at 48kHz)
- A bidirectional audio bridge that transcodes and resamples between the two
Prerequisites
- Go 1.23+
- A running StreamCoreAI server with a WHIP endpoint
Configuration
Copy and edit the config file:
cp config.toml.example config.toml
Key settings in config.toml:
[sip]
listen_addr = "0.0.0.0:5060" # SIP UDP listen address
external_ip = "" # Public IP for SDP (auto-detected if empty)
rtp_port_min = 10000
rtp_port_max = 20000
[streamcoreai]
whip_endpoint = "http://localhost:8080/whip"
token_url = "http://localhost:8080/token"
api_key = "sk-streamcore-demo-key"
[api]
listen_addr = "0.0.0.0:8090"
[providers.twilio]
enabled = false
trunk_domain = "your-trunk.pstn.twilio.com"
[providers.ringcentral]
enabled = false
outbound_proxy = "sip.ringcentral.com:5060"
JWT Authentication
When the StreamCoreAI server has jwt_secret set, the SIP server needs to fetch a JWT to authenticate WHIP requests. Add token_url and api_key under [streamcoreai]:
[streamcoreai]
whip_endpoint = "http://localhost:8080/whip"
token_url = "http://localhost:8080/token"
api_key = "sk-streamcore-demo-key"
Caller Identity
Every call forwards the far end's number to StreamCoreAI as its resource_id — the caller on an inbound call, the callee on an outbound one. An external agent (bring your own agent) receives it alongside each turn and can use it to scope memory to the person, so a repeat caller is recognised instead of starting over.
No configuration is required. With token_url set, the number is signed into the JWT as a claim; without it, the SIP server sends it as an X-StreamCore-Resource-Id header, which the StreamCoreAI server accepts from trusted server-side callers like this one.
Running
go build -o sip-server .
./sip-server # uses config.toml in current directory
./sip-server /path/to/config.toml
REST API
Health Check
GET /health
List Providers
GET /providers
List Calls
GET /calls
Get Call
GET /calls/{id}
Initiate Outgoing Call
POST /calls
Content-Type: application/json
{
"to": "+15551234567",
"provider": "twilio"
}
Hang Up Call
DELETE /calls/{id}
Providers
Asterisk (Local Testing)
A local Asterisk PBX is included for end-to-end testing — see Local Testing with Asterisk below.
Twilio
Configure your Twilio Elastic SIP Trunk domain. Point the trunk's origination URI to your SIP server's address.
RingCentral
Configure the RingCentral outbound proxy and credentials.
Local Testing with Asterisk
A Dockerized Asterisk PBX is included so you can test the full call flow locally without any external SIP providers.
Prerequisites
- Docker & Docker Compose
- A running StreamCoreAI server on
localhost:8080(or via the rootdocker-compose.yml) - A SIP softphone app (e.g. Oher, Zoiper, Oher Mobile, Linphone)
Architecture
Softphone (6001) StreamCoreAI Server
│ │
│ SIP/RTP (PCMU) │ WHIP (Opus/WebRTC)
▼ ▼
┌──────────┐ SIP/RTP (PCMU) ┌──────────────────┐
│ Asterisk │◄────────────────────►│ SIP Server │
│ (PBX) │ │ (this project) │
└──────────┘ └──────────────────┘
172.20.0.10 172.20.0.20
Quick Start
1. Start Asterisk + SIP Server:
cd sip-server
docker compose up --build
This starts:
- Asterisk on
172.20.0.10:5060(mapped to hostlocalhost:5060/udp) - SIP Server on
172.20.0.20:5060(mapped to hostlocalhost:5061/udp) with REST API onlocalhost:8090
Make sure your StreamCoreAI server is running on
localhost:8080before starting.
2. Configure your softphone:
Register with the Asterisk server using these credentials:
| Setting | Value |
|---|---|
| SIP Server | localhost (or your machine IP) |
| SIP Port | 5060 |
| Username | 6001 |
| Password | test123 |
| Transport | UDP |
A second extension 6002 / test123 is also available.
3. Test an incoming call (phone → StreamCoreAI):
From your softphone, dial 7000. Asterisk routes the call to the SIP server, which connects to StreamCoreAI. You should hear the AI agent respond.
4. Test an outgoing call (StreamCoreAI → phone):
Use the REST API to initiate a call from StreamCoreAI to your softphone:
curl -X POST http://localhost:8090/calls \
-H "Content-Type: application/json" \
-d '{"to": "6001", "provider": "asterisk"}'
Your softphone should ring. Answer it to talk to the AI agent.
5. Manage calls:
# List active calls
curl http://localhost:8090/calls
# Get call details
curl http://localhost:8090/calls/<call-id>
# Hang up a call
curl -X DELETE http://localhost:8090/calls/<call-id>
# Check health
curl http://localhost:8090/health
# List providers
curl http://localhost:8090/providers
Running Without Docker
If you want to run just Asterisk in Docker and the SIP server natively:
# Start only Asterisk
docker compose up asterisk --build
# Run SIP server locally (uses config.toml which points to localhost)
go build -o sip-server .
./sip-server
The default config.toml has the asterisk provider pointing to localhost:5060.
Asterisk Extensions
| Extension | Description |
|---|---|
7000 | Routes to the StreamCoreAI SIP server |
6001 | Test softphone extension 1 |
6002 | Test softphone extension 2 |
9999 | Echo test (plays back your audio for debugging) |
Troubleshooting
- No audio: Make sure the RTP port ranges don't conflict. Asterisk uses
10000-10100, SIP server uses10200-10300in Docker mode. - Registration failed: Check that Asterisk is running (
docker compose logs asterisk) and the credentials match. - "provider not configured": Make sure
providers.asterisk.enabled = truein your config. - StreamCoreAI connection fails: Verify StreamCoreAI is running at the configured WHIP endpoint. In Docker, it uses
host.docker.internal:8080. - The agent interrupts itself: a reply gets cut mid-sentence, nothing arrives to replace it, and the call sits in dead air. There is no AEC anywhere on a carrier path, so the agent's own voice comes back attenuated but structurally identical to speech and the energy VAD calls it a barge-in. This server sends
aec=noneon every call, and StreamCoreAI's defaultpipeline.echo_guard = "auto"turns on an echo bound for exactly those sessions while leaving browser sessions on the same instance alone. If you still see it, check the server log forecho guard onat call setup, and confirmecho_guardhas not been set to"off". Raising the VAD threshold instead only trades this for being deaf to quiet callers. - Inbound goes silent while the agent speaks: this looks like half-duplex media and sends people hunting through the RTP reader, the transcode, and the caller's handset. The usual cause is DTX — G.729 Annex B or Opus DTX suppressing "silent" RTP, which on a call with an agent talking means all inbound during playback. Force G.711 (PCMU/PCMA) end to end. This server already advertises PCMU only; check the PBX and the trunk for the other legs. No amount of VAD tuning helps when the frames are never sent.
How It Works
Incoming Calls
- SIP INVITE arrives → server sends 100 Trying
- Parses remote SDP for RTP address
- Creates a StreamCoreAI SDK connection via WHIP
- Allocates a local RTP port and responds with 200 OK + SDP
- Starts bidirectional audio bridge (PCMU ↔ Opus)
Outgoing Calls
- REST API request triggers
POST /calls - Creates a StreamCoreAI SDK connection
- Sends SIP INVITE to the provider's trunk with local SDP
- On 200 OK, sends ACK and starts audio bridge
Audio Bridge
- SIP → StreamCoreAI: Decode G.711 μ-law → upsample 8kHz to 48kHz → encode Opus → send via WebRTC
- StreamCoreAI → SIP: Decode Opus → downsample 48kHz to 8kHz → encode G.711 μ-law → send via RTP