Chapter 715m

Making outbound calls

Making outbound calls

Everything so far has been about receiving calls -- patients dial your number and reach your AI receptionist. But what about the other direction? A dental office needs to call patients too: appointment confirmations, reminders, follow-ups. In this chapter, you will configure an outbound SIP trunk and use the CreateSIPParticipant API to make your agent call a patient's phone.

Outbound trunkCreateSIPParticipantOutbound calling

What you'll learn

  • How outbound SIP trunks differ from inbound trunks
  • How to configure an outbound trunk via the CLI and API
  • How to use CreateSIPParticipant to place a call programmatically
  • How to build an appointment confirmation callback feature

Inbound vs outbound trunks

Inbound trunks handle calls coming in from the phone network. Outbound trunks handle calls going out to the phone network. The direction is reversed, but the concept is the same: a trunk is the bridge between LiveKit and the SIP provider.

An outbound trunk needs:

  • Name -- a human-readable label
  • Address -- your SIP trunk provider's address (where to send outbound SIP INVITE messages)
  • Numbers -- the caller ID numbers that will appear on the recipient's phone
  • Authentication -- credentials to authenticate with your trunk provider
What's happening

Think of inbound trunks as the door people walk through to reach you, and outbound trunks as the door you walk through to reach them. They are separate configurations because the authentication, routing, and billing are different for each direction. Your trunk provider charges differently for inbound and outbound minutes.

Creating an outbound trunk via CLI

terminalbash
lk sip trunk create-outbound \
--name "Dental Office Outbound" \
--address "sip:trunk.provider.com" \
--numbers "+15551234567" \
--auth-username "your-username" \
--auth-password "your-password"

This returns an outbound trunk ID (e.g., ST_outbound_xxx). The --address is your SIP provider's endpoint, and --numbers is the caller ID that recipients will see.

Verify the trunk:

terminalbash
lk sip trunk list

Creating an outbound trunk via the API

Python

setup_outbound.pypython
import asyncio
from livekit.api import (
  LiveKitAPI,
  CreateSIPOutboundTrunkRequest,
  SIPOutboundTrunkInfo,
)


async def main():
  api = LiveKitAPI()

  trunk = await api.sip.create_sip_outbound_trunk(
      CreateSIPOutboundTrunkRequest(
          trunk=SIPOutboundTrunkInfo(
              name="Dental Office Outbound",
              address="sip:trunk.provider.com",
              numbers=["+15551234567"],
              auth_username="your-username",
              auth_password="your-password",
          )
      )
  )

  print(f"Created outbound trunk: {trunk.sip_trunk_id}")

  await api.aclose()


asyncio.run(main())

TypeScript

setup-outbound.tstypescript
import { SipClient } from "livekit-server-sdk";

const sipClient = new SipClient(
process.env.LIVEKIT_URL!,
process.env.LIVEKIT_API_KEY!,
process.env.LIVEKIT_API_SECRET!
);

const trunk = await sipClient.createSipOutboundTrunk({
name: "Dental Office Outbound",
address: "sip:trunk.provider.com",
numbers: ["+15551234567"],
authUsername: "your-username",
authPassword: "your-password",
});

console.log(`Created outbound trunk: ${trunk.sipTrunkId}`);

Protect trunk credentials

The auth_username and auth_password are your SIP trunk provider credentials. Never hardcode them in source files. Use environment variables or a secrets manager. Anyone with these credentials could place calls on your account.

Making a call with CreateSIPParticipant

The CreateSIPParticipant API is how you programmatically place an outbound call. It does three things in one operation:

  1. Creates a LiveKit room (or joins an existing one)
  2. Places a SIP call to the specified phone number via your outbound trunk
  3. Adds the called party to the room as a SIP participant when they answer
make_call.pypython
import asyncio
from livekit.api import LiveKitAPI, CreateSIPParticipantRequest


async def call_patient(patient_phone: str, room_name: str):
  api = LiveKitAPI()

  participant = await api.sip.create_sip_participant(
      CreateSIPParticipantRequest(
          sip_trunk_id="ST_outbound_xxx",
          sip_call_to=patient_phone,
          room_name=room_name,
          participant_identity="patient-callback",
      )
  )

  print(f"Call placed to {patient_phone}")
  print(f"Participant SID: {participant.participant_id}")

  await api.aclose()


# Call a patient to confirm their appointment
asyncio.run(call_patient("+15559876543", "outbound-confirmation-001"))
make-call.tstypescript
import { SipClient } from "livekit-server-sdk";

const sipClient = new SipClient(
process.env.LIVEKIT_URL!,
process.env.LIVEKIT_API_KEY!,
process.env.LIVEKIT_API_SECRET!
);

async function callPatient(patientPhone: string, roomName: string) {
const participant = await sipClient.createSipParticipant({
  sipTrunkId: "ST_outbound_xxx",
  sipCallTo: patientPhone,
  roomName: roomName,
  participantIdentity: "patient-callback",
});

console.log(`Call placed to ${patientPhone}`);
console.log(`Participant SID: ${participant.participantId}`);
}

// Call a patient to confirm their appointment
await callPatient("+15559876543", "outbound-confirmation-001");
1

API sends CreateSIPParticipant request

Your backend calls the LiveKit API with the outbound trunk ID, the phone number to call, and the room to place the call in.

2

LiveKit places the SIP call

LiveKit's SIP bridge sends a SIP INVITE to your trunk provider, which routes the call through the PSTN to the patient's phone.

3

Patient's phone rings

The patient sees your dental office number on their caller ID and answers.

4

Patient joins the room

Once the patient answers, they are added to the specified room as a ParticipantKind.SIP participant. Your agent (already in the room or dispatched to it) can now talk to them.

Building an appointment confirmation agent

Here is a complete example that combines an outbound call with an agent that confirms an appointment:

confirmation_agent.pypython
from livekit.agents import Agent, AgentSession, AgentServer, function_tool, RunContext
from livekit.plugins import openai, deepgram, cartesia

server = AgentServer()


@function_tool
async def confirm_appointment(context: RunContext) -> str:
  """Called when the patient confirms their appointment."""
  # In production, update your scheduling database here
  return "Appointment confirmed successfully."


@function_tool
async def cancel_appointment(context: RunContext) -> str:
  """Called when the patient wants to cancel or reschedule."""
  # In production, update your scheduling database here
  return "Appointment cancelled. The office will call back to reschedule."


@server.rtc_session
async def entrypoint(session: AgentSession):
  await session.start(
      agent=Agent(
          instructions="""You are calling from Bright Smile Dental to confirm an
          appointment. The patient has an appointment tomorrow at 2:00 PM with
          Dr. Chen for a routine cleaning.

          Greet the patient, identify yourself as calling from Bright Smile Dental,
          and ask them to confirm their appointment for tomorrow at 2 PM.

          If they confirm, use the confirm_appointment tool.
          If they want to cancel or reschedule, use the cancel_appointment tool.

          Be brief and professional. After confirming or cancelling, thank them
          and say goodbye.""",
          tools=[confirm_appointment, cancel_appointment],
      ),
      room=session.room,
      stt=deepgram.STT(model="nova-3"),
      llm=openai.LLM(model="gpt-4o-mini"),
      tts=cartesia.TTS(voice="<voice-id>"),
  )


if __name__ == "__main__":
  server.run()

To trigger the outbound call, a separate script (or your backend system) calls CreateSIPParticipant with a room name like outbound-confirmation-001. The agent is dispatched to that room and waits. When the patient answers, the agent greets them and runs the confirmation flow.

What's happening

The outbound pattern flips the usual flow. Instead of the patient calling and triggering room creation via a dispatch rule, your backend creates the room and places the call. The agent still joins the room the same way -- it is dispatched when a room matching its pattern is created. The only difference is who initiated the room creation.

Outbound calls need an agent in the room

Make sure your agent is registered to handle the room name pattern you use for outbound calls (e.g., outbound-*). The agent must be ready to join before or shortly after the patient answers. If there is no agent when the patient picks up, they hear silence.

Handling voicemail and no-answer

Not every outbound call will be answered by a human. You should handle common scenarios:

  • No answer -- the call times out. Your backend receives a failure status and can retry later or mark the patient for follow-up.
  • Voicemail -- some voicemail systems answer the call (triggering the agent). Consider adding voicemail detection logic or instructing the agent to leave a brief message if it detects an answering machine greeting.
  • Busy signal -- the call is rejected. Retry with exponential backoff.

Respect calling regulations

Outbound calls to patients are subject to regulations like the TCPA (Telephone Consumer Protection Act) in the United States. Ensure you have the patient's prior consent to call, honor do-not-call requests, and restrict calls to appropriate hours. Consult legal counsel for compliance requirements in your jurisdiction.

Test your knowledge

Question 1 of 3

How does the outbound call flow differ from the inbound call flow in terms of room creation?

What you learned

  • Outbound SIP trunks enable your agent to place calls to phone numbers
  • CreateSIPParticipant places a call, creates or joins a room, and adds the called party as a SIP participant
  • Outbound calls use the same room/participant/agent architecture as inbound calls
  • An appointment confirmation agent combines outbound calling with task-specific instructions and tools
  • Handle voicemail, no-answer, and busy scenarios in production

Next up

You have built the complete telephony system -- inbound and outbound. In the final chapter, you will test everything end-to-end by calling your agent from your actual phone and using Cloud Insights to monitor call quality.

Concepts covered
Outbound trunkCreateSIPParticipantOutbound calling