Skip to content
Learn Agentic AI13 min read0 views

SIP Trunking for AI Agents: Connecting to Traditional Phone Systems

Master SIP trunking to connect AI agents to PBX systems and the PSTN. Covers SIP protocol basics, trunk configuration, DTMF handling, and intelligent call routing for enterprise telephony.

What Is SIP Trunking and Why AI Agents Need It

Session Initiation Protocol (SIP) is the backbone of modern telephony. While APIs like Twilio abstract the phone network behind HTTP calls, enterprises with existing PBX systems (Asterisk, FreeSWITCH, Cisco CUCM) need to connect AI agents directly via SIP trunks. This gives you lower latency, more control over audio streams, and integration with existing call infrastructure.

A SIP trunk is a virtual phone line that connects your system to the public telephone network (PSTN) or to another SIP endpoint. For AI agents, a SIP trunk lets you receive and make calls through existing enterprise phone systems without replacing the entire infrastructure.

SIP Protocol Fundamentals for Developers

A SIP call follows a clear lifecycle. Understanding it is essential for debugging and building reliable integrations:

INVITE → 100 Trying → 180 Ringing → 200 OK → ACK → [Media/RTP] → BYE → 200 OK

The critical detail for AI agents is that signaling (SIP) and media (RTP) travel on separate paths. SIP handles call setup, teardown, and metadata. RTP carries the actual audio. Your AI agent needs to handle both.

Configuring a SIP Trunk with Python

Here is how to register a SIP trunk programmatically and handle incoming calls using the pjsua2 library (the Python wrapper for PJSIP):

See AI Voice Agents Handle Real Calls

Book a free demo or calculate how much you can save with AI voice automation.

import pjsua2 as pj

class AIAgentAccount(pj.Account):
    """SIP account that routes incoming calls to the AI agent."""

    def onIncomingCall(self, prm):
        call = AICall(self, prm.callId)
        call_prm = pj.CallOpParam(True)
        call_prm.statusCode = 200  # Answer the call
        call.answer(call_prm)


class AICall(pj.Call):
    """Handles a single SIP call with AI processing."""

    def onCallState(self, prm):
        ci = self.getInfo()
        print(f"Call state: {ci.stateText}")

        if ci.state == pj.PJSIP_INV_STATE_DISCONNECTED:
            print("Call ended")

    def onCallMediaState(self, prm):
        ci = self.getInfo()
        for mi in ci.media:
            if mi.type == pj.PJMEDIA_TYPE_AUDIO and                mi.status == pj.PJSUA_CALL_MEDIA_ACTIVE:
                # Audio is flowing — connect to AI pipeline
                audio_media = self.getAudioMedia(mi.index)
                self.start_ai_processing(audio_media)

    def start_ai_processing(self, audio_media):
        """Connect audio stream to the AI transcription pipeline."""
        # Capture audio from the caller
        recorder = pj.AudioMediaRecorder()
        recorder.createRecorder("/tmp/call_audio.wav")
        audio_media.startTransmit(recorder)
        print("AI processing started on audio stream")


def setup_sip_agent(sip_server, username, password):
    """Initialize the SIP stack and register with the server."""
    ep = pj.Endpoint()
    ep.libCreate()

    ep_cfg = pj.EpConfig()
    ep_cfg.uaConfig.maxCalls = 10
    ep.libInit(ep_cfg)

    # Create a UDP transport for SIP signaling
    transport_cfg = pj.TransportConfig()
    transport_cfg.port = 5060
    ep.transportCreate(pj.PJSIP_TRANSPORT_UDP, transport_cfg)

    ep.libStart()

    # Register with the SIP server
    acc_cfg = pj.AccountConfig()
    acc_cfg.idUri = f"sip:{username}@{sip_server}"
    acc_cfg.regConfig.registrarUri = f"sip:{sip_server}"

    cred = pj.AuthCredInfo("digest", "*", username, 0, password)
    acc_cfg.sipConfig.authCreds.append(cred)

    account = AIAgentAccount()
    account.create(acc_cfg)
    print(f"Registered as {username}@{sip_server}")

    return ep, account

DTMF Handling Over SIP

DTMF (Dual-Tone Multi-Frequency) tones are the signals generated when callers press keys on their phone. SIP supports two DTMF methods: in-band (audio tones in the RTP stream) and RFC 2833 (out-of-band events). Always prefer RFC 2833 — it is more reliable:

class AICallWithDTMF(pj.Call):
    """Extended call handler with DTMF support."""

    def __init__(self, account, call_id):
        super().__init__(account, call_id)
        self.dtmf_buffer = ""

    def onDtmfDigit(self, prm):
        digit = prm.digit
        self.dtmf_buffer += digit
        print(f"DTMF received: {digit} (buffer: {self.dtmf_buffer})")

        # Check for complete input sequences
        if digit == "#":
            self.process_dtmf_input(self.dtmf_buffer)
            self.dtmf_buffer = ""

    def process_dtmf_input(self, sequence):
        """Route based on DTMF input."""
        routing_map = {
            "1#": "sales",
            "2#": "support",
            "3#": "billing",
            "0#": "operator",
        }
        destination = routing_map.get(sequence, "ai_agent")
        print(f"Routing to: {destination}")

Intelligent Call Routing with SIP

One of the most powerful patterns is using AI to make routing decisions before transferring calls through the SIP infrastructure:

import asyncio
from openai import AsyncOpenAI

openai_client = AsyncOpenAI()

async def ai_route_decision(caller_id, initial_transcript):
    """Use AI to determine the best routing destination."""
    response = await openai_client.chat.completions.create(
        model="gpt-4o",
        messages=[
            {
                "role": "system",
                "content": (
                    "You are a call routing agent. Based on the caller's "
                    "initial statement, return one of: sales, support, "
                    "billing, emergency, or ai_self_service."
                ),
            },
            {"role": "user", "content": initial_transcript},
        ],
        max_tokens=20,
    )
    destination = response.choices[0].message.content.strip().lower()
    return destination


def transfer_call_sip(call, destination_extension):
    """Perform a SIP REFER to transfer the call."""
    transfer_uri = f"sip:{destination_extension}@pbx.company.com"
    prm = pj.CallOpParam()
    call.xfer(transfer_uri, prm)
    print(f"Transferred call to {transfer_uri}")

Production Security

SIP trunks require careful security configuration. Always use SIP over TLS (SIPS) for signaling encryption and SRTP for media encryption. Implement IP whitelisting on your SIP server to accept connections only from known trunk providers. Use strong authentication credentials and rotate them regularly.

FAQ

What is the difference between SIP trunking and a Twilio API?

SIP trunking gives you a direct connection to the phone network using standard telephony protocols, while Twilio wraps those protocols behind HTTP APIs. SIP trunking offers lower latency and tighter integration with existing PBX systems but requires more infrastructure management. Twilio is easier to start with but costs more per call and adds a layer of abstraction.

How do I handle NAT traversal issues with SIP?

NAT is the most common source of SIP problems. Use a STUN/TURN server to discover your public IP, configure your SIP stack to include the public IP in SDP messages, and ensure your firewall allows RTP traffic on the configured port range (typically UDP 10000-20000). Many SIP providers also support TCP or WebSocket transport, which avoids most NAT issues.

Can I run multiple AI agents on a single SIP trunk?

Yes. A SIP trunk supports multiple concurrent calls (limited by your trunk provider's channel count). Each incoming call triggers a new INVITE, and your application creates a new call handler instance. Use the maxCalls configuration to limit concurrency and prevent overload on your AI processing pipeline.


#SIP #Telephony #PBX #VoiceAI #CallRouting #Enterprise #AgenticAI #LearnAI #AIEngineering

Share this article
C

CallSphere Team

Expert insights on AI voice agents and customer communication automation.

Try CallSphere AI Voice Agents

See how AI voice agents work for your industry. Live demo available -- no signup required.