Overview
Make outbound calls from LiveKit rooms to phone numbers by creating SIP participants. When you create a SIP participant with an outbound trunk, LiveKit initiates a call to the specified phone number and connects the callee to the room as a SIP participant. Once connected, the callee can interact with other participants in the room, including AI agents and regular participants.
To make outbound calls, you need at least one outbound trunk configured. You can customize outbound calls with features like custom caller ID, DTMF tones for extension codes, and dial tone playback while the call connects.
To create an AI agent to make outbound calls on your behalf, see the Voice AI quickstart.
Creating a SIP participant
To make outbound calls with SIP Service, create a SIP participant with the CreateSIPParticipant API. It returns an SIPParticipantInfo object that describes the participant.
Outbound calling requires at least one Outbound Trunk.
Create a
sip-participant.jsonfile with the following participant details:{"sip_trunk_id": "<your-trunk-id>","sip_call_to": "<phone-number-to-dial>","room_name": "my-sip-room","participant_identity": "sip-test","participant_name": "Test Caller","krisp_enabled": true,"wait_until_answered": true}Create the SIP Participant using the CLI. After you run this command, the participant makes a call to the
sip_call_tonumber configured in your outbound trunk. When you setwait_until_answeredtotrue, the command waits until the callee picks up the call before returning. You can also monitor the call status using the SIP participant attributes. When the callee picks up the call, thesip.callStatusattribute isactive.lk sip participant create sip-participant.json
import { SipClient, TwirpError } from 'livekit-server-sdk';const sipClient = new SipClient(process.env.LIVEKIT_URL,process.env.LIVEKIT_API_KEY,process.env.LIVEKIT_API_SECRET);// Outbound trunk to use for the callconst trunkId = '<your-trunk-id>';// Phone number to dialconst phoneNumber = '<phone-number-to-dial>';// Name of the room to attach the call toconst roomName = 'my-sip-room';const sipParticipantOptions = {participantIdentity: 'sip-test',participantName: 'Test Caller',krispEnabled: true,waitUntilAnswered: true};async function main() {try {const participant = await sipClient.createSipParticipant(trunkId,phoneNumber,roomName,sipParticipantOptions);console.log('Participant created:', participant);} catch (error) {console.error('Error creating SIP participant:', error);if (error instanceof TwirpError) {console.error("SIP error code: ", error.metadata?.['sip_status_code']);console.error("SIP error message: ", error.metadata?.['sip_status']);}}}main();
import asynciofrom livekit import apifrom livekit.protocol.sip import CreateSIPParticipantRequest, SIPParticipantInfoasync def main():livekit_api = api.LiveKitAPI()request = CreateSIPParticipantRequest(sip_trunk_id = "<trunk_id>",sip_call_to = "<phone_number>",room_name = "my-sip-room",participant_identity = "sip-test",participant_name = "Test Caller",krisp_enabled = True,wait_until_answered = True)try:participant = await livekit_api.sip.create_sip_participant(request)print(f"Successfully created {participant}")except Exception as e:print(f"Error creating SIP participant: {e}")# sip_status_code contains the status code from upstream carrierprint(f"SIP error code: {e.metadata.get('sip_status_code')}")# sip_status contains the status message from upstream carrierprint(f"SIP error message: {e.metadata.get('sip_status')}")finally:await livekit_api.aclose()asyncio.run(main())
require 'livekit'trunk_id = "<trunk_id>";number = "<phone_number>";room_name = "my-sip-room";participant_identity = "sip-test";participant_name = "Test Caller";sip_service = LiveKit::SIPServiceClient.new(ENV['LIVEKIT_URL'],api_key: ENV['LIVEKIT_API_KEY'],api_secret: ENV['LIVEKIT_API_SECRET'])resp = sip_service.create_sip_participant(trunk_id,number,room_name,participant_identity: participant_identity,participant_name: participant_name)puts resp.data
package mainimport ("context""fmt""os"lksdk "github.com/livekit/server-sdk-go/v2""github.com/livekit/protocol/livekit")func main() {trunkId := "<trunk_id>";phoneNumber := "<phone_number>";roomName := "my-sip-room";participantIdentity := "sip-test";participantName := "Test Caller";request := &livekit.CreateSIPParticipantRequest {SipTrunkId: trunkId,SipCallTo: phoneNumber,RoomName: roomName,ParticipantIdentity: participantIdentity,ParticipantName: participantName,KrispEnabled: true,WaitUntilAnswered: true,}sipClient := lksdk.NewSIPClient(os.Getenv("LIVEKIT_URL"),os.Getenv("LIVEKIT_API_KEY"),os.Getenv("LIVEKIT_API_SECRET"))// Create trunkparticipant, err := sipClient.CreateSIPParticipant(context.Background(), request)if err != nil {fmt.Println(err)} else {fmt.Println(participant)}}
import io.livekit.server.CreateSipParticipantOptionsimport io.livekit.server.SipServiceClientval sipClient = SipServiceClient.createClient(System.getenv("LIVEKIT_URL") ?: "",System.getenv("LIVEKIT_API_KEY") ?: "",System.getenv("LIVEKIT_API_SECRET") ?: "")val trunkId = "<trunk_id>"val phoneNumber = "<phone_number>"val roomName = "my-sip-room"val options = CreateSipParticipantOptions(participantIdentity = "sip-test",participantName = "Test Caller",waitUntilAnswered = true)var participant: LivekitSip.SIPParticipantInfo? = nulltry {val response = sipClient.createSipParticipant(trunkId,phoneNumber,roomName,options ).execute()if (response.isSuccessful) {participant = response.body()}} catch (e: Exception) {println("Error creating SIP participant: ${e.message}")}
Once the user picks up, they are connected to my-sip-room.
Agent initiated outbound calls
To have your agent make an outbound call, dispatch the agent and then create a SIP participant. This section describes how to modify the voice AI quickstart for outbound calling. Alternatively, see the following complete example on GitHub:
Outbound caller example
Complete example of an outbound calling agent.
Dialing a number
Add the following code to the agent code from the voice AI quickstart. Your agent reads the phone number passed in the metadata field of the agent dispatch request and places an outbound call by creating a SIP participant.
You should also remove the initial greeting or place it behind an if statement to ensure the agent waits for the user to speak first when placing an outbound call.
You must add a valid outbound trunk ID to successfully make a phone call. To see a list of your outbound trunks use the LiveKit CLI: lk sip outbound list.
Add the following code to the agent.py file from the Voice AI quickstart:
# add these imports at the top of your filefrom livekit import agents, apiimport json# ... AgentServer, Assistant class, and AgentSession config from the voice AI quickstart ...@server.rtc_session(agent_name="my-telephony-agent")async def my_agent(ctx: agents.JobContext):# If a phone number was provided, then place an outbound call# By having a condition like this, you can use the same agent for inbound/outbound telephony as well as web/mobile/etc.dial_info = json.loads(ctx.job.metadata)phone_number = dial_info.get("phone_number")# The participant's identity can be anything you want, but this example uses the phone number itselfsip_participant_identity = phone_numberif phone_number is not None:# The outbound call will be placed after this method is executedtry:await ctx.api.sip.create_sip_participant(api.CreateSIPParticipantRequest(# This ensures the participant joins the correct roomroom_name=ctx.room.name,# This is the outbound trunk ID to use# You can get this from LiveKit CLI with `lk sip outbound list`sip_trunk_id='ST_xxxx',# The outbound phone number to dial and identity to usesip_call_to=phone_number,participant_identity=sip_participant_identity,# This waits until the call is answered before returningwait_until_answered=True,))print("call picked up successfully")except api.TwirpError as e:print(f"error creating SIP participant: {e.message}, "f"SIP status: {e.metadata.get('sip_status_code')} "f"{e.metadata.get('sip_status')}")ctx.shutdown()return# Create and start your AgentSession# session = AgentSession(...)# await session.start(room=ctx.room, agent=Assistant(), ...)# When placing an outbound call, let the callee speak first.if phone_number is None:await session.generate_reply(instructions="Greet the user and offer your assistance.")
Install livekit-server-sdk:
pnpm add livekit-server-sdk
Then, edit the main.ts file from the voice AI quickstart. Add the outbound dial logic at the top of entry, before creating the session. Make sure to use a valid ID for the outboundTrunkId. Run lk sip outbound list to get a list of outbound trunks.
import { SipClient } from 'livekit-server-sdk';// ... any existing code / imports ...const outboundTrunkId = '<outbound-trunk-id>';const sipRoom = 'new-room';export default defineAgent({prewarm: async (proc: JobProcess) => {proc.userData.vad = await silero.VAD.load();},entry: async (ctx: JobContext) => {// If a phone number was provided, place an outbound call.const dialInfo = JSON.parse(ctx.job.metadata || '{}');const phoneNumber = dialInfo.phone_number;if (phoneNumber) {const sipClient = new SipClient(process.env.LIVEKIT_URL,process.env.LIVEKIT_API_KEY,process.env.LIVEKIT_API_SECRET,);try {await sipClient.createSipParticipant(outboundTrunkId,phoneNumber,sipRoom,{participantIdentity: phoneNumber,participantName: 'Test callee',waitUntilAnswered: true,},);console.log('Call picked up successfully');} catch (error) {console.error('Error creating SIP participant:', error);ctx.shutdown();return;}}// Create and start your AgentSession (use your existing STT, LLM, TTS config from the quickstart)// Only greet first on inbound; on outbound, the recipient speaks first and the agent responds after their turn.if (!phoneNumber) {session.generateReply({instructions: 'Greet the user and offer your assistance.',});}},});// Update the agentName from the quickstart to "my-telephony-agent"cli.runApp(new ServerOptions({ agent: fileURLToPath(import.meta.url), agentName: 'my-telephony-agent' }));
Start the agent and follow the instructions in the next section to call your agent.
Make a call with your agent
Use either the LiveKit CLI or the Python API to instruct your agent to place an outbound phone call.
In this example, the job's metadata includes the phone number to call. You can extend this to include more information if needed for your use case.
The agent name must match the name you assigned to your agent. If you set it earlier in the agent dispatch section, this is my-telephony-agent.
Make sure to verify or update the values in the following examples:
- Room name: The examples use
new-room. - Agent name: Must match the name you assigned to your agent.
- Phone number: Provide a valid phone number to dial.
The following command creates a new room and dispatches your agent to it with the phone number to call.
lk dispatch create \--new-room \--agent-name my-telephony-agent \--metadata '{"phone_number": "+15105550123"}' # insert your own phone number here
await lkapi.agent_dispatch.create_dispatch(api.CreateAgentDispatchRequest(# Use the agent name you set in the rtc_session decoratoragent_name="my-telephony-agent",# The room name to use.room="new-room",# Here we use JSON to pass the phone number, and could add more information if needed.metadata='{"phone_number": "+15105550123"}'))
import { AgentDispatchClient } from 'livekit-server-sdk';const agentDispatchClient = new AgentDispatchClient(process.env.LIVEKIT_URL!,process.env.LIVEKIT_API_KEY!,process.env.LIVEKIT_API_SECRET!,);// Use the agent name you set in ServerOptions.agentName. Room must match the name used for CreateSIPParticipant (e.g. new-room).await agentDispatchClient.createDispatch('new-room', // must match the room name used when creating the SIP participant'my-telephony-agent',{ metadata: '{"phone_number": "+15105550123"}' },);
Voicemail detection
Your agent might encounter an automated system such as an answering machine or voicemail. You can give your LLM the ability to detect a likely voicemail system via tool call, and then perform special actions such as leaving a message and hanging up.
import asyncio # add this import at the top of your fileclass Assistant(Agent):## ... existing init code ...@function_toolasync def detected_answering_machine(self):"""Call this tool if you have detected a voicemail system, AFTER hearing the voicemail greeting"""await self.session.generate_reply(instructions="Leave a voicemail message letting the user know you'll call back later.")await asyncio.sleep(0.5) # Add a natural gap to the end of the voicemail messageawait hangup_call()
class VoicemailAgent extends voice.Agent {constructor() {super({// ... existing init code ...tools: {leaveVoicemail: llm.tool({description: 'Call this tool if you detect a voicemail system, AFTER you hear the voicemail greeting',execute: async (_, { ctx }: llm.ToolOptions) => {const handle = ctx.session.generateReply({instructions:"Leave a brief voicemail message for the user telling them you are sorry you missed them, but you will call back later. You don't need to mention you're going to leave a voicemail, just say the message",});handle.addDoneCallback(() => {setTimeout(async () => {await hangUpCall();}, 500);});},}),}})}}
Custom caller ID
You can set a custom caller ID for outbound calls using the display_name field in theCreateSIPParticipant request. By default, if this field isn't included in the request, the phone number is used as the display name. If this field is set to an empty string, most SIP trunking providers issue a Caller ID Name (CNAM) lookup and use the result as the display name.
Your SIP provider must support custom caller ID for the display_name value to be used. Confirm with your specific provider to verify support.
{"sip_trunk_id": "<your-trunk-id>","sip_call_to": "<phone-number-to-dial>","room_name": "my-sip-room","participant_identity": "sip-test","participant_name": "Test Caller","display_name": "My Custom Display Name"}
const sipParticipantOptions = {participantIdentity: 'sip-test',participantName: 'Test Caller',displayName: 'My Custom Display Name'};
request = CreateSIPParticipantRequest(sip_trunk_id = "<trunk_id>",sip_call_to = "<phone_number>",room_name = "my-sip-room",participant_identity = "sip-test",participant_name = "Test Caller",display_name = "My Custom Display Name")
Custom display name is not yet supported in Ruby.
displayName := "My Custom Display Name"request := &livekit.CreateSIPParticipantRequest {SipTrunkId: trunkId,SipCallTo: phoneNumber,RoomName: roomName,ParticipantIdentity: participantIdentity,ParticipantName: participantName,KrispEnabled: true,WaitUntilAnswered: true,DisplayName: &displayName,}
Custom display name is not yet supported in Kotlin.
Making a call with extension codes (DTMF)
To make outbound calls with fixed extension codes (DTMF tones), set dtmf field in CreateSIPParticipant request:
{"sip_trunk_id": "<your-trunk-id>","sip_call_to": "<phone-number-to-dial>","dtmf": "*123#ww456","room_name": "my-sip-room","participant_identity": "sip-test","participant_name": "Test Caller"}
const sipParticipantOptions = {participantIdentity: 'sip-test',participantName: 'Test Caller',dtmf: '*123#ww456'};
request = CreateSIPParticipantRequest(sip_trunk_id = "<trunk_id>",sip_call_to = "<phone_number>",room_name = "my-sip-room",participant_identity = "sip-test",participant_name = "Test Caller",dtmf = "*123#ww456")
resp = sip_service.create_sip_participant(trunk_id,number,room_name,participant_identity: participant_identity,participant_name: participant_name,dtmf: "*123#ww456")
request := &livekit.CreateSIPParticipantRequest{SipTrunkId: trunkId,SipCallTo: phoneNumber,RoomName: roomName,ParticipantIdentity: participantIdentity,ParticipantName: participantName,Dtmf: "*123#ww456",}
val options = CreateSipParticipantOptions(participantIdentity = "sip-test",participantName = "Test Caller",dtmf = "*123#ww456")sipClient.createSipParticipant(trunkId, phoneNumber, roomName, options).execute()
Character w can be used to delay DTMF by 0.5 sec.
This example dials a specified number and sends the following DTMF tones:
*123#- Wait 1 sec
456
Playing dial tone while the call is dialing
SIP participants emit no audio by default while the call connects. This can be changed by setting play_dialtone field in CreateSIPParticipant request:
{"sip_trunk_id": "<your-trunk-id>","sip_call_to": "<phone-number-to-dial>","room_name": "my-sip-room","participant_identity": "sip-test","participant_name": "Test Caller","play_dialtone": true}
const sipParticipantOptions = {participantIdentity: 'sip-test',participantName: 'Test Caller',playDialtone: true};
request = CreateSIPParticipantRequest(sip_trunk_id = "<trunk_id>",sip_call_to = "<phone_number>",room_name = "my-sip-room",participant_identity = "sip-test",participant_name = "Test Caller",play_dialtone = True)
resp = sip_service.create_sip_participant(trunk_id,number,room_name,participant_identity: participant_identity,participant_name: participant_name,play_dialtone: true)
request := &livekit.CreateSIPParticipantRequest{SipTrunkId: trunkId,SipCallTo: phoneNumber,RoomName: roomName,ParticipantIdentity: participantIdentity,ParticipantName: participantName,PlayDialtone: true,}
val options = CreateSipParticipantOptions(participantIdentity = "sip-test",participantName = "Test Caller",playDialtone = true)
If play_dialtone is enabled, the SIP Participant plays a dial tone to the room until the phone is picked up.
Hang up
To let your agent end the call for all participants, add the prebuilt EndCallTool to your agent's tools (Python only). The tool shuts down the session and can delete the room to disconnect everyone. If the agent session ends but the room is not deleted, the user continues to hear silence until they hang up.
For a custom implementation or Node.js, use the delete_room API. The following example implements a basic hangup_call function you can use as a starting point:
# Add these imports at the top of your filefrom livekit import api, rtcfrom livekit.agents import get_job_context# Add this function definition anywhereasync def hangup_call():ctx = get_job_context()if ctx is None:# Not running in a job contextreturnawait ctx.api.room.delete_room(api.DeleteRoomRequest(room=ctx.room.name,))class MyAgent(Agent):...# to hang up the call as part of a function call@function_toolasync def end_call(self, ctx: RunContext):"""Called when the user wants to end the call"""await ctx.wait_for_playout() # let the agent finish speakingawait hangup_call()
import { RoomServiceClient } from 'livekit-server-sdk';import { getJobContext } from '@livekit/agents';const hangUpCall = async () => {const jobContext = getJobContext();if (!jobContext) {return;}const roomServiceClient = new RoomServiceClient(process.env.LIVEKIT_URL!,process.env.LIVEKIT_API_KEY!,process.env.LIVEKIT_API_SECRET!);if (jobContext.room.name) {await roomServiceClient.deleteRoom(jobContext.room.name,);}}class MyAgent extends voice.Agent {constructor() {super({instructions: 'You are a helpful voice AI assistant.',// ... existing code ...tools: {hangUpCall: llm.tool({description: 'Call this tool if the user wants to hang up the call.',execute: async (_, { ctx }: llm.ToolOptions<UserData>) => {await hangUpCall();return "Hung up the call";},}),},});}}