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.
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
- Go 1.23+
- A running StreamCoreAI server with a WHIP endpoint
Copy and edit the config file:
cp config.toml.example config.tomlKey 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"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"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.
go build -o sip-server .
./sip-server # uses config.toml in current directory
./sip-server /path/to/config.tomlGET /health
GET /providers
GET /calls
GET /calls/{id}
POST /calls
Content-Type: application/json
{
"to": "+15551234567",
"provider": "twilio"
}
DELETE /calls/{id}
A local Asterisk PBX is included for end-to-end testing — see Local Testing with Asterisk below.
Configure your Twilio Elastic SIP Trunk domain. Point the trunk's origination URI to your SIP server's address.
Configure the RingCentral outbound proxy and credentials.
A Dockerized Asterisk PBX is included so you can test the full call flow locally without any external SIP providers.
- 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)
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
1. Start Asterisk + SIP Server:
cd sip-server
docker compose up --buildThis 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/providersIf 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-serverThe default config.toml has the asterisk provider pointing to localhost:5060.
| 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) |
- 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.
- 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)
- 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
- 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