Why Bolting Twilio Onto LiveKit Breaks in Production
Call state diverges between two systems
Twilio shows a call ringing while LiveKit reports it ended. Production calls drop at one-second durations because neither system owns the truth.
SIP transfers fail through the bridge
SIP REFER returns 603 errors through the Twilio-to-LiveKit bridge. Voice-triggered transfers fail without error logs. Developers wait months for fixes.
Two bills for one phone call
Twilio charges for SIP trunking. LiveKit charges for audio orchestration. At 22,000 monthly calls, connectivity alone costs $860/month before any AI processing.
Two dashboards when a call fails
Debugging a dropped call means correlating LiveKit logs with Twilio logs across two platforms, two credential sets, and two support channels.
Build a Voice AI Agent
fromsignalwire_agentsimportAgentBasefromsignalwire_agents.core.function_resultimportSwaigFunctionResultclassSupportAgent(AgentBase):def__init__(self):super().__init__(name="Support Agent",route="/support")self.prompt_add_section("Instructions",body="You are a customer support agent. ""Greet the caller and resolve their issue.")self.add_language("English","en-US","rime.spore:mistv2")@AgentBase.tool(name="check_order")defcheck_order(self,order_id:str):"""Check the status of a customer order. Args: order_id: The order ID to look up """returnSwaigFunctionResult(f"Order {order_id}: shipped, ETA April 2nd")agent=SupportAgent()agent.run()
import{AgentBase,FunctionResult}from'@signalwire/sdk';constagent=newAgentBase({name:'Support Agent',route:'/support',});agent.promptAddSection('Instructions','You are a customer support agent. Greet the caller and resolve their issue.');agent.addLanguage({name:'English',code:'en-US',voice:'rime.spore:mistv2'});agent.defineTool({name:'check_order',description:'Check the status of a customer order',parameters:{type:'object',properties:{order_id:{type:'string',description:'The order ID to look up'},},required:['order_id'],},handler:(args)=>{returnnewFunctionResult(`Order ${args.order_id}: shipped, ETA April 2nd`);},});agent.run();
packagemainimport("fmt""github.com/signalwire/signalwire-go/pkg/agent""github.com/signalwire/signalwire-go/pkg/swaig")funcmain(){a:=agent.NewAgentBase(agent.WithName("Support Agent"),agent.WithRoute("/support"),)a.PromptAddSection("Instructions","You are a customer support agent. Greet the caller and resolve their issue.")a.AddLanguage(map[string]any{"name":"English","code":"en-US","voice":"rime.spore:mistv2",})a.DefineTool(agent.ToolDefinition{Name:"check_order",Description:"Check the status of a customer order",Parameters:map[string]any{"type":"object","properties":map[string]any{"order_id":map[string]any{"type":"string","description":"The order ID to look up",},},"required":[]string{"order_id"},},Handler:func(argsmap[string]any,rawDatamap[string]any)*swaig.FunctionResult{orderID:=args["order_id"]returnswaig.NewFunctionResult(fmt.Sprintf("Order %v: shipped, ETA April 2nd",orderID),)},})a.Run()}
importcom.signalwire.sdk.agent.AgentBase;importcom.signalwire.sdk.swaig.FunctionResult;importjava.util.List;importjava.util.Map;publicclassSupportAgent{publicstaticvoidmain(String[]args)throwsException{varagent=AgentBase.builder().name("Support Agent").route("/support").build();agent.promptAddSection("Instructions","You are a customer support agent. "+"Greet the caller and resolve their issue.");agent.addLanguage("English","en-US","rime.spore:mistv2");agent.defineTool("check_order","Check the status of a customer order",Map.of("type","object","properties",Map.of("order_id",Map.of("type","string","description","The order ID to look up")),"required",List.of("order_id")),(toolArgs,rawData)->{varorderId=toolArgs.get("order_id");returnnewFunctionResult("Order "+orderId+": shipped, ETA April 2nd");});agent.run();}}
# frozen_string_literal: truerequire'signalwire'agent=SignalWire::AgentBase.new(name:'Support Agent',route:'/support')agent.prompt_add_section('Instructions','You are a customer support agent. Greet the caller and resolve their issue.')agent.add_language(name:'English',code:'en-US',voice:'rime.spore:mistv2')agent.define_tool(name:'check_order',description:'Check the status of a customer order',parameters:{'order_id'=>{'type'=>'string','description'=>'The order ID to look up'}})do|args,_raw|SignalWire::Swaig::FunctionResult.new("Order #{args['order_id']}: shipped, ETA April 2nd")endagent.run
<?phprequire'vendor/autoload.php';useSignalWire\Agent\AgentBase;useSignalWire\SWAIG\FunctionResult;$agent=newAgentBase(['name'=>'Support Agent','route'=>'/support']);$agent->promptAddSection('Instructions','You are a customer support agent. Greet the caller and resolve their issue.');$agent->addLanguage('English','en-US','rime.spore:mistv2');$agent->defineTool(name:'check_order',description:'Check the status of a customer order',parameters:['order_id'=>['type'=>'string','description'=>'The order ID to look up'],],handler:function(array$args):FunctionResult{returnnewFunctionResult("Order {$args['order_id']}: shipped, ETA April 2nd");});$agent->run();
#!/usr/bin/env perlusestrict;usewarnings;uselib'lib';useSignalWire::Agent::AgentBase;useSignalWire::SWAIG::FunctionResult;my$agent=SignalWire::Agent::AgentBase->new(name=>'Support Agent',route=>'/support',);$agent->prompt_add_section('Instructions','You are a customer support agent. Greet the caller and resolve their issue.');$agent->add_language(name=>'English',code=>'en-US',voice=>'rime.spore:mistv2');$agent->define_tool(name=>'check_order',description=>'Check the status of a customer order',parameters=>{order_id=>{type=>'string',description=>'The order ID to look up'},},handler=>sub{my($args,$raw)=@_;returnSignalWire::SWAIG::FunctionResult->new(response=>"Order $args->{order_id}: shipped, ETA April 2nd");},);$agent->run;
#include<signalwire/agent/agent_base.hpp>usingnamespacesignalwire;usingjson=nlohmann::json;classSupportAgent:publicagent::AgentBase{public:SupportAgent():AgentBase("Support Agent","/support"){prompt_add_section("Instructions","You are a customer support agent. ""Greet the caller and resolve their issue.");add_language({"English","en-US","rime.spore:mistv2"});define_tool({.name="check_order",.description="Check the status of a customer order",.parameters={{"order_id",{{"type","string"},{"description","The order ID to look up"}}}},.handler=[](constjson&args,constjson&){autoorder_id=args.value("order_id","unknown");returnswaig::FunctionResult("Order "+order_id+": shipped, ETA April 2nd");}});}};intmain(){SupportAgent().run();}
usingSignalWire.Agent;usingSignalWire.SWAIG;varagent=newAgentBase(newAgentOptions{Name="Support Agent",Route="/support"});agent.PromptAddSection("Instructions","You are a customer support agent. Greet the caller and resolve their issue.");agent.AddLanguage("English","en-US","rime.spore:mistv2");agent.DefineTool("check_order","Check the status of a customer order",new{type="object",properties=new{order_id=new{type="string",description="The order ID to look up"}},required=new[]{"order_id"}},(args,rawData)=>{varorderId=args.TryGetValue("order_id",outvarid)?id:"unknown";returnnewFunctionResult($"Order {orderId}: shipped, ETA April 2nd");});agent.Run();
usesignalwire::agent::AgentBase;usesignalwire::swaig::FunctionResult;useserde_json::json;fnmain(){letmutagent=AgentBase::builder().name("Support Agent").route("/support").build();agent.prompt_add_section("Instructions","You are a customer support agent. Greet the caller and resolve their issue.",&[]).add_language("English","en-US","rime.spore:mistv2");agent.define_tool("check_order","Check the status of a customer order",json!({"type":"object","properties":{"order_id":{"type":"string","description":"The order ID to look up"}},"required":["order_id"]}),Box::new(|args,_raw|{letorder_id=args.get("order_id").and_then(|v|v.as_str()).unwrap_or("unknown");FunctionResult::with_response(&format!("Order {order_id}: shipped, ETA April 2nd"))}),);agent.run();}
Phone Number Capabilities
LiveKit Phone Numbers
US-only coverage
Inbound only, no outbound calling
No call transfers of any type
No number porting
No international numbers
Requires a second provider for production telephony
SignalWire
Global coverage across dozens of countries
Inbound and outbound calling included
All transfer types: cold, warm, blind, attended
Number porting to bring existing numbers
Local and toll-free numbers worldwide
One platform, one bill, no bridge
Feature Matrix
Capability
LiveKit Phone Numbers
LiveKit + Twilio/Telnyx
SignalWire
Countries available
US only
Depends on CPaaS provider
Global
Inbound calling
Yes
Yes
Yes
Outbound calling
No
Yes (via CPaaS)
Yes (native)
Cold transfers
No
SIP REFER fails (603 errors)
Native
Warm transfers
No
Fragile through bridge
Native
Number porting
Not documented
Via CPaaS provider
Yes
Call recording
Not included
Separate config per provider
Built in
Call state ownership
LiveKit only
Split across two systems
Single source of truth
Billing
LiveKit only
Two vendors, two invoices
One vendor, one invoice
Setup complexity
Low (US inbound only)
High (multi-provider config)
Low (native platform)
We didn't add SIP support as an afterthought. We wrote the SIP stack the industry runs on. FreeSWITCH has processed trillions of carrier minutes across nearly 20 years of production deployment.
From LiveKit + Twilio to One Platform
1
Install the SDK
Run `pip install signalwire-agents` to get telephony, AI, and agent framework in one package.
2
Define your agent
Same AI logic, new SDK surface. AgentBase replaces VoicePipelineAgent with built-in transfers and state management.
3
Port your numbers
Bring your existing phone numbers to SignalWire. Global coverage, inbound and outbound, all transfer types.
4
Retire the second provider
No more Twilio bill, no more bridge latency, no more split call state. One platform handles everything.
A Dev.to analysis of 22,000 monthly voice AI calls found $860/month in connectivity costs alone when using LiveKit + Twilio, before any AI processing. SignalWire provides AI processing at $0.16/min with transport billed separately at carrier rates. One platform, one invoice.
FAQ
Can I port my existing phone numbers to SignalWire?
Yes. SignalWire supports number porting for US and international numbers. Your existing numbers move to the platform with no downtime for your callers.
Does SignalWire support outbound calling for campaigns?
Yes. Outbound calling is native to the platform. Appointment reminders, lead follow-ups, and campaign calls work with the same agent framework and phone numbers.
What transfer types does SignalWire support?
All of them: cold, warm, blind, and attended. Transfers are built into the platform and work with agent-to-agent, agent-to-queue, and agent-to-external-number scenarios.
Why does LiveKit need a separate telephony provider?
LiveKit is an agent framework built on WebRTC. Phone calls require SIP and PSTN connectivity, which LiveKit does not include. A bridge to Twilio or Telnyx adds latency, cost, and failure modes.
Trusted by 2,000+ companies
Global Phone Numbers. Native Transfers. One Platform.
Stop bridging two providers for what should be one phone call.