ambitious-london-8261
12/22/2025, 6:33 AMtall-airline-16756
12/22/2025, 6:33 AMambitious-london-8261
12/22/2025, 6:33 AMtall-airline-16756
12/22/2025, 6:33 AM200 OK from Asterisk when the customer answers but your agent never starts speaking, and you want to avoid creating a second room/channel.
Short answer: have the agent wait for the SIP participant to join the same LiveKit room (or wait until the call is answered) and then trigger the agent to speak — do not create a second call. Two ways to achieve this:
1) Use wait_until_answered=True when creating the SIP participant so your agent flow pauses until Asterisk returns 200 OK, then continue your agent logic in the same room.
• This ensures the SIP participant joins the existing room before you start playback or TTS.
2) Listen for the participant_connected (or equivalent SIP participant join) event and start the agent’s audio only after you detect the SIP participant in the room.
• Filter for SIP participants (participant.kind == SIP) or check sip attributes, then call your speech/TTS/send-audio routine.
Recommended flow to avoid double rooms:
1. Dispatch agent into room A.
2. Agent calls create_sip_participant for the customer with wait_until_answered=True (or create participant and wait for participant_connected).
3. When you receive the participant-connected event (or your wait_until_answered returns), start the agent speaking in room A.
If you share which agent SDK/language you’re using (Python, JS, etc.) and a short snippet of how you currently create the SIP participant, I can give a concrete code example to plug into your flow.
Sources: Agents telephony integration | LiveKit docs | SIP Lifecycle Management Agent | LiveKit docstall-airline-16756
12/22/2025, 6:34 AMwait_until_answered=True flow and triggering speech on the room participant_connected event — that option is available for Python agents. If you're on Python I can share the exact code snippets to add; if you use Node or another runtime I’ll suggest the closest alternative.ambitious-london-8261
12/22/2025, 6:37 AMtall-airline-16756
12/22/2025, 6:37 AMawait ctx.api.sip.create_sip_participant(
api.CreateSIPParticipantRequest(
room_name=ctx.room.name,
sip_trunk_id="ST_xxxx", # your trunk ID
sip_call_to="+15105550123", # destination number
participant_identity="caller",
wait_until_answered=True,
)
)
2) Listen for the room participant join and trigger the agent reply when the SIP participant connects:
@ctx.room.on("participant_connected")
async def on_participant_connected(participant: rtc.RemoteParticipant):
# detect SIP participant
if participant.kind == rtc.ParticipantKind.PARTICIPANT_KIND_SIP:
# optional: check identity or metadata if you need to filter specific calls
await session.generate_reply(
instructions="Hello, thank you for answering. How can I assist you today?"
)
Notes:
• Keep the agent connected to the same room before creating the SIP participant so the SIP leg joins that room on answer.
• Avoid creating another call/room — using wait_until_answered=True plus participant_connected ensures a single room and that speech starts only after the customer answers.
If you’d like, I can provide a complete minimal agent file showing connection, dispatch, and these handlers.ambitious-london-8261
12/22/2025, 6:44 AMtall-airline-16756
12/22/2025, 6:44 AMparticipant_connected event.
How it maps to your steps:
• Agent originates to Asterisk (agent stays in room).
• Asterisk dials the customer via carrier and, when the customer answers, sends 200 OK to LiveKit. LiveKit treats that as the SIP participant joining the room.
• Using wait_until_answered=True will block until that 200 OK arrives (so your code waits for answer) and/or you can rely on the participant_connected event to trigger the agent speech once the SIP participant joins.
Quick checks to ensure success:
• Verify the agent connects to the room before calling create_sip_participant.
• Confirm Asterisk’s 200 OK reaches LiveKit and that the SIP participant is created with the expected kind/identity (you can log participant metadata).
• Use participant_connected to start speech (or follow up right after the create_sip_participant call if you use wait_until_answered=True).
If you want, I can share a minimal full agent example that includes room connect, create_sip_participant(..., wait_until_answered=True), and the participant_connected handler so you can drop it into your setup. Which would you prefer — full example or just the handler + call snippet?ambitious-london-8261
12/22/2025, 6:47 AMtall-airline-16756
12/22/2025, 6:48 AMwait_until_answered=True, waits for the 200 OK / SIP participant to join the same room, then speaks. This avoids creating a second room or extra calls.
Save as outbound_agent.py and update your .env.
import asyncio
import json
import logging
import os
from dotenv import load_dotenv
from livekit import api, rtc
from livekit.agents import AgentServer, JobContext, VoiceAssistantConfig, llm, stt, tts
from livekit.agents.voice_assistant import VoiceAssistant
from livekit.plugins import silero
load_dotenv()
logging.basicConfig(level=<http://logging.INFO|logging.INFO>)
logger = logging.getLogger("outbound-agent")
server = AgentServer()
def prewarm(proc):
proc.userdata["vad"] = silero.VAD.load()
server.setup_fnc = prewarm
class OutboundAgent(VoiceAssistant):
def __init__(self, ctx: JobContext):
super().__init__(
job_context=ctx,
assistant_name="Outbound Caller",
instructions="You are an outbound caller. Greet the user when they answer.",
config=VoiceAssistantConfig(
pre_processor=llm.PromptFnc(llm.prompt_default),
model=llm.LLM(model="gpt-4o-mini"),
stt=stt.DeepgramSTT(model="nova-3"),
tts=tts.CartesiaTTSElement(voice="british english male 1"),
vad=ctx.proc.userdata["vad"],
),
)
@server.rtc_session()
async def entrypoint(ctx: JobContext):
<http://logger.info|logger.info>(f"Agent started for room {ctx.room.name}")
# read phone number from job metadata
metadata = json.loads(ctx.job.metadata or "{}")
phone_number = metadata.get("phone_number")
if not phone_number:
logger.error("phone_number missing in job metadata")
return
sip_trunk_id = os.getenv("SIP_OUTBOUND_TRUNK_ID")
if not sip_trunk_id:
logger.error("SIP_OUTBOUND_TRUNK_ID not set")
return
# Ensure agent is connected to the room (optional: keep connected before dialing)
await ctx.connect(auto_subscribe=api.AutoSubscribe.AUDIO_ONLY)
<http://logger.info|logger.info>(f"Dialing {phone_number} via trunk {sip_trunk_id}")
try:
# This call blocks until 200 OK is received (call answered)
await ctx.api.sip.create_sip_participant(
api.CreateSIPParticipantRequest(
room_name=ctx.room.name,
sip_trunk_id=sip_trunk_id,
sip_call_to=phone_number,
participant_identity=f"called-{phone_number}",
wait_until_answered=True,
play_dialtone=True,
)
)
except Exception as e:
logger.error(f"Failed to create SIP participant: {e}")
return
<http://logger.info|logger.info>("Call answered — starting assistant speech")
assistant = OutboundAgent(ctx)
# TTS greeting will be played into the room (and therefore to the SIP leg)
await assistant.say("Hello, thank you for answering. How can I help you today?")
# Keep session alive until disconnected
await ctx.wait_for_disconnected()
<http://logger.info|logger.info>("Session ended")
if __name__ == "__main__":
server.run()
.env example:
LIVEKIT_URL=<wss://your-instance.livekit.cloud>
LIVEKIT_API_KEY=your-api-key
LIVEKIT_API_SECRET=your-api-secret
SIP_OUTBOUND_TRUNK_ID=ST_XXXXXXXXXXX
Notes and checklist for your Asterisk flow:
• Agent must be in the same LiveKit room before/during the outbound SIP create call so the SIP leg joins that room on answer.
• Asterisk must forward the 200 OK to LiveKit so the SIP participant is created/joined; wait_until_answered=True makes the agent code block until that 200 OK arrives.
• Use the participant_connected event or the post-create_sip_participant continuation (as shown) to start speaking — both ensure speech only starts after answer.
• No second room is created with this flow.
If you want, I can:
• Provide a version that logs participant attributes when the SIP leg joins, or
• Add a participant_connected handler instead of relying on the blocking call. Which would you prefer? Sources: Outbound Calling Script | LiveKit docs | SIP Lifecycle Management Agent | LiveKit docs | Agents telephony integration | LiveKit docsambitious-london-8261
12/22/2025, 7:07 AMtall-airline-16756
12/22/2025, 7:07 AM200 OK itself carries the SDP answer (the session description), not RTP media. RTP media normally starts after the 200 OK is exchanged and the ACK is completed. So Asterisk does not send RTP "inside" the 200 OK; it sends the SDP in the 200 OK and then begins sending RTP packets once the SIP handshake (200 OK → ACK) is complete and media is established.
For your flow with LiveKit + the Python agent:
• LiveKit needs the SIP 200 OK with SDP so the SIP participant can join the room.
• RTP media from Asterisk to LiveKit should begin after the 200 OK/ACK handshake. If you see no audio after answer, check that Asterisk actually starts sending RTP to LiveKit after the ACK and that LiveKit receives it.
Would you like help verifying the SDP/RTP exchange (examples of the 200 OK SDP to look for or packet-level checks to run)?ambitious-london-8261
12/22/2025, 7:08 AMtall-airline-16756
12/22/2025, 7:08 AMambitious-london-8261
12/29/2025, 7:30 AMtall-airline-16756
12/29/2025, 7:30 AMtcpdump -w outbound_to_livekit.pcap host <livekit_sip_ip> and port 5060 or udp
◦ Verify RTP packets are sent from LiveKit and reach Asterisk, and vice‑versa.
• NAT / public IP in SDP
◦ Ensure Asterisk isn’t advertising a private IP in the SDP. If behind NAT, configure external_media_address / rtp_symmetric / stun settings so LiveKit can send RTP back to correct IP:port.
• Symmetric RTP / pinholing
◦ If LiveKit sends RTP from a different IP/port than the one Asterisk sent to, Asterisk may drop it unless symmetric RTP is enabled. Enable symmetric RTP or configure static RTP peers.
• RTP port ranges and binding
◦ Confirm Asterisk’s RTP port range (rtp.conf) and that those ports aren’t blocked by firewall/NAT.
• DTLS/SRTP / Secure media mismatch
◦ If one side expects SRTP/DTLS and the other doesn’t, media will stop. Verify if LiveKit requires secure media for that trunk and match it on Asterisk.
• Call state / ACK completeness
◦ Ensure the SIP handshake completes (INVITE → 200 OK → ACK). You said ACK is now present for incoming; verify ACK is seen for outbound flows as well.
What to collect and share so I can dig deeper
• The 200 OK SDP sent by LiveKit and the SDP Asterisk sends (redact secrets).
• A short pcap (tcpdump) covering INVITE → 200 OK → ACK → first 10–20 seconds afterwards.
• Asterisk RTP settings: rtp.conf (port range), any NAT settings (external_media_address, rtp_symmetric), and whether you use chan_sip or pjsip.
• Whether your trunk/LiveKit uses secure SIP/media (TLS/SRTP) or plain UDP.
Immediate quick tests you can run
1. From Asterisk host, run tcpdump while calling and confirm LiveKit RTP packets arrive:
◦ tcpdump -n -s 0 -w sip_rtp.pcap udp and host <livekit_sip_ip>
1. Check Asterisk console for RTP activity and audio levels:
◦ rtp set debug on or pjsip set logger on depending on driver.
1. Temporarily enable symmetric RTP in Asterisk (if using chan_sip/pjsip) and retest.
If you prefer, share the 200 OK SDP and a small pcap and I’ll inspect for:
• Mismatched codecs
• Wrong IP/port in c= or m= lines
• RTP packets being sent but dropped/misrouted
Which of those logs can you provide first — the 200 OK SDP or a short pcap?ambitious-london-8261
12/29/2025, 7:30 AMtall-airline-16756
12/29/2025, 7:30 AMexternal_media_address, rtp_symmetric), and whether you use chan_sip or pjsip.
While you gather that, please try these quick checks and share results:
• tcpdump on Asterisk during the call: tcpdump -n -s 0 -w outbound_livekit.pcap udp and host <livekit_sip_ip> — confirm whether LiveKit RTP reaches Asterisk and whether Asterisk sends RTP back.
• Verify Asterisk is advertising a public IP in SDP (no private IPs in c= line).
• Confirm codec overlap (opus/PCMU/PCMA) present in both SDPs.
• Ensure symmetric RTP is enabled (or static peers) so Asterisk accepts RTP from LiveKit if source IP/port differs.
Send the 200 OK SDP or the pcap and your Asterisk config and I’ll inspect for mismatched IP/ports, codec issues, SRTP/DTLS mismatch, or transaction problems.ambitious-london-8261
12/29/2025, 7:31 AMtall-airline-16756
12/29/2025, 7:32 AMlocalnet for incoming, the root cause is Asterisk advertising/handling local addresses for media. Quick actionable steps to fix and verify:
1. Ensure public IP is advertised in SDP
• Set externip (or external_media_address) so Asterisk uses your public IP in SDP.
◦ chan_sip: set externip=your.public.ip and keep localnet=...
◦ pjsip: set external_media_address / external_signaling_address in transport
1. Enable NAT-friendly behavior
• Set nat=yes and force_rport=yes (chan_sip) or ensure rport=yes behavior in pjsip.
• Consider rtp_symmetric=yes so Asterisk accepts RTP from the source LiveKit actually uses.
1. Disable direct media/hairpinning for those trunks
• pjsip: direct_media=no (or canreinvite=no for chan_sip). This forces Asterisk to relay media and avoids attempts to use private endpoints directly.
1. Confirm RTP port ranges & firewall
• Ensure Asterisk RTP port range is open and not overwritten by localnet rules.
• Verify no firewall/NAT device rewrites or blocks LiveKit RTP ports.
1. Test and capture:
• With localnet configured, capture SIP/SDP + RTP on Asterisk:
◦ tcpdump: tcpdump -n -s 0 -w trace.pcap udp and host <livekit_sip_ip> or port 5060
• Inspect SDP c= and m= lines in both directions. If c= shows private IP for the media address, that’s the cause.
1. Temporary experiment that often proves the issue
• Keep localnet present but set externip to your public IP and rtp_symmetric=yes. If outbound RTP works then, the problem was private SDP addresses.
What to send me next if you want deeper inspection
• The 200 OK SDP from LiveKit and the SDP Asterisk generates (redact secrets), or
• A short pcap covering INVITE → 200 OK → ACK → first 10–20s of RTP.
• Your Asterisk RTP/NAT settings (rtp.conf, transport config, and whether chan_sip or pjsip).
If you want, I can give the exact config lines for chan_sip or pjsip based on which driver you run. Which driver are you using (chan_sip or pjsip)?
Sources: sip | livekit/livekit_composite | DeepWiki | Firewall Tips | How We Built a Globally Distributed WebRTC Mesh Network | Outbound Calling Script | LiveKit docs