glamorous-oil-12326
02/18/2026, 12:38 AMtall-airline-16756
02/18/2026, 12:38 AMtall-airline-16756
02/18/2026, 12:38 AMwait_until_answered: true.
iii. When the call is answered, start room composite egress to record.
1. If you want recording to start automatically when the external participant joins, use Auto‑Egress on your dispatch rule for SIP outbound calls. That will trigger recording at the moment the call connects so egress and the live call are synchronized.
Why this fixes it
• If egress joins before the remote party, recordings may start with gaps or audio only from the agent; starting egress after connection ensures the recorder sees both sides from the same point in time.
If you’d like, tell me:
• Which SDK/language you’re using (Python, JS, etc.), and
• Whether you prefer manual control or Auto‑Egress tied to dispatch rules.
I can then provide a short, concrete code example for your setup or the exact dispatch rule configuration to enable Auto‑Egress.
Sources: examples/bank-ivr | livekit/livekit_composite | DeepWiki | Python SDKs | livekit/livekit_composite | DeepWiki | Why is there extra audio at the end of my recordings of an agent session? |...glamorous-oil-12326
02/18/2026, 12:49 AMtall-airline-16756
02/18/2026, 12:49 AMegress_started webhook).
4. If confirmation received → create SIP participant with wait_until_answered: true.
5. If egress failed → abort dialing and handle error.
Python sketch:
# 1) Start egress
egress_resp = await livekit_api.egress.start_room_composite_egress(
StartRoomCompositeEgressRequest(
room_name=room_name,
output=RoomCompositeEgressRequest(
file=EncodedFileOutput(
file_type=EncodedFileType.MP4,
filepath=f"recordings/{room_name}.mp4",
# s3 config...
)
)
)
)
# 2) Confirm egress started (either from egress_resp or from webhook)
if egress_started: # true only after confirmation
# 3) Dial
await livekit_api.sip.create_sip_participant(
CreateSIPParticipantRequest(
room_name=room_name,
sip_trunk_id=sip_trunk_id,
sip_call_to=phone_number,
participant_identity=f"caller_{phone_number}",
wait_until_answered=True,
)
)
else:
# Do not dial — log/alert and clean up
pass
Notes and hardening:
• Use the egress API response where it includes an egress id and status. For maximum reliability, treat the API start response as tentative and rely on the egress_started webhook event to confirm the recorder is actively running.
• Implement a timeout: if no egress_started event arrives within N seconds, treat as failure and do not dial.
• If your process must be atomic, keep the dial attempted only after webhook confirmation; this ensures the remote party is never called if egress fails.
2) Auto‑Egress (what it does and its limitation for your use case)
• Core idea: configure the SIP dispatch rule to include egress configuration. When the SIP participant/dispatch rule runs and the call connects, LiveKit will automatically start egress for that room.
• Important constraint: Auto‑Egress does not give you a way to block the outbound dial if egress fails. Recording is started automatically when the call connects; if egress fails to start, the call may still have been placed. Therefore auto‑egress cannot guarantee “never dial unless recording started.”
Example dispatch rule snippet (JSON)
{
"dispatch_rule": {
"trunk_ids": [
"TR_XXX"
],
"rule": {
"dispatchRuleIndividual": {
"roomPrefix": "call-"
}
},
"room_config": {
"egress": {
"room_composite": {
"outputs": [
{
"file": {
"filepath": "recordings/{{.Room}}.mp4",
"s3": {
"bucket": "your-bucket"
}
}
}
]
}
}
}
}
}
• Apply via your dispatch configuration method (CLI/management API).
• Behavior: when an outbound SIP call routed by that rule connects, LiveKit will create the room and start room composite egress automatically.
Recommendation summary
• Use manual egress control if your requirement is strict: do not dial unless you have firm confirmation that egress is actively recording.
• Use webhook confirmation (listen for egress_started) rather than only the API start response for the strongest guarantee.
• Use auto‑egress only if you prefer an automated, simpler setup and you accept that a dial may occur even if egress later fails.
If you’d like, tell me:
• Which SDK/language you want a complete working code sample for (JS/Node, Python, or Go).
• Whether you already have webhook handling in place (so I can show the exact webhook event to wait for and sample handling logic).
I’ll provide a ready-to-run example tailored to your stack (including webhook handling and timeouts) so you can enforce “do not dial unless egress confirmed.”glamorous-oil-12326
02/18/2026, 12:51 AMtall-airline-16756
02/18/2026, 12:51 AMstart_room_composite_egress, start_track_composite_egress, or another?
4. Do you rely on the API start response to confirm egress, or do you wait for a webhook/event (for example egress_started)?
5. Are you using wait_until_answered=True when creating the SIP participant?
6. Can you share the small snippet that triggers egress and the snippet that creates the SIP participant (sanitized keys/IDs is fine)?
Meanwhile, common causes and concrete fixes to try now
• Cause: egress is starting before the SIP participant is fully connected (or before the agent’s media is actually flowing). That can make the recorder capture early silence or pre-call agent audio, which appears as the agent starting early.
◦ Fix: Start egress only after you have event confirmation that both relevant participants are present and actively publishing audio:
▪︎ Wait for the SIP participant’s connected/joined event (or wait_until_answered=True plus participant_joined webhook) and the agent’s participant_joined event, then start egress.
▪︎ Prefer using the egress_started webhook as the definitive confirmation of active recording rather than only the immediate start API response.
• Cause: Using room composite egress can capture a hidden recorder participant join time that doesn’t align with when tracks begin sending audio.
◦ Fix: Try track_composite_egress or explicitly select the agent’s and caller’s tracks so recording starts relative to actual track timestamps.
• Cause: Race conditions between starting egress and the agent’s publishing of audio (agent appears earlier in the recording timeline).
◦ Fixes:
▪︎ Add an explicit short verification step after participants join: check participant state (is_publishing audio track) before starting egress.
▪︎ Use a small startup buffer: only start egress once both sides have an active audio track (or after receiving a few milliseconds of audio-level activity).
• Reliability tip: Use webhooks + timeout
◦ Wait for the participant_joined (or equivalent) webhook for both agent and SIP caller, then start egress. If no webhook within your timeout, abort dialing or retry egress.
◦ If you already start egress first to guard dialing, keep egress start but only dial after receiving egress_started confirmation (this avoids dialing when egress failed) and still wait for actual publishing before treating the recording as valid.
If you tell me the SDK and share the short code paths for:
• how you start egress, and
• how/when you create the SIP participant
I’ll give a concrete code sample (node/python) showing: 1) waiting for participant track/publish events, 2) starting egress at the right moment (or using track_composite_egress), and 3) robust webhook/t imeout handling so you never dial unless recording is confirmed and recordings are synchronized.
Sources: Best practices for managing webhook event streams | LiveKit | Universal Egress | Turns overview | LiveKit Documentation | server-sdk-ruby | livekit/livekit_composite | DeepWiki | Outbound Calling Script | LiveKit Documentationglamorous-oil-12326
02/18/2026, 12:53 AMsession_started = asyncio.create_task(
session.start(
agent=agent,
record=True,
room=ctx.room,
room_input_options=RoomInputOptions(
# enable Krisp background voice and noise removal
noise_cancellation=noise_cancellation.BVCTelephony(),
),
)
)
4. Then I dial the end participanttall-airline-16756
02/18/2026, 12:53 AMegress_started webhook AND for both participants to be publishing audio before considering the recording valid.
• Use wait_until_answered=True when creating the SIP participant so the create call only returns once the remote answered.
• Prefer tracking actual publishing state (participant events or checking published tracks) rather than purely timing-based waits.
• Consider track_composite_egress if you need tighter control of which tracks/timestamps are recorded.
Concrete Python pattern (high-level)
1. Dispatch and ensure agent enters the room. Wait for agent’s participant_joined and confirm they are publishing audio (or at least ready).
2. Start egress OR prepare to start it, but do not dial yet.
3. Create the SIP participant with wait_until_answered=True so the call creation only returns once the remote answered.
4. After the SIP participant is connected, verify both participants have active audio tracks (either via participant state or via an event listener).
5. Only then consider the recording start confirmed. If you started egress earlier, wait for egress_started webhook before dialing; if you start egress now, call the start API and wait for its confirmation.
6. If any of the verification steps fail (egress fails to start, SIP call not answered, no track publishing), abort dialing or retry as appropriate.
Example sketch (Python, async) — pattern to adapt to your code
# Pseudocode / structural example — adapt to your existing clients and handlers
async def start_call_with_strict_egress_guard(lk_api, room_name, agent, phone_number, sip_trunk_id):
# 1) Dispatch agent and wait for agent join + publishing
await dispatch_agent_to_room(agent, room_name)
await wait_for_participant_join(room_name, agent.identity, timeout=10)
await wait_for_participant_publishing_audio(room_name, agent.identity, timeout=5)
# 2) (Optional) start egress now to reserve egress and get an egress_id
# If you start egress now, do NOT dial until you receive egress_started webhook.
try:
egress_resp = await lk_api.egress.start_room_composite_egress(
StartRoomCompositeEgressRequest(
room_name=room_name,
output=..., # your output settings
)
)
egress_id = egress_resp.egress_id
except Exception as e:
# Egress failed to start — do not dial
log("egress failed to start, aborting dial", e)
return
# 3) Create SIP participant but ensure the call is answered before returning
sip_resp = await lk_api.sip.create_sip_participant(
CreateSIPParticipantRequest(
room_name=room_name,
sip_trunk_id=sip_trunk_id,
sip_call_to=phone_number,
participant_identity=f"caller_{phone_number}",
wait_until_answered=True, # critical: block until remote answers
)
)
# 4) Confirm the SIP participant joined AND is publishing audio
caller_identity = sip_resp.participant_identity # or the identity you passed
await wait_for_participant_join(room_name, caller_identity, timeout=10)
await wait_for_participant_publishing_audio(room_name, caller_identity, timeout=5)
# 5) Confirm egress actually started (if you started it earlier) via webhook event
# You should have a webhook handler that sets egress_confirmed[egress_id] = True
if not await wait_for_egress_confirmed(egress_id, timeout=10):
# Egress did not confirm — hang up / cleanup / do not treat recording as valid
log("egress didn't confirm, tearing down call and cleaning up")
await cleanup_room(room_name)
return
# At this point both participants are connected, publishing, and egress recording is confirmed.
log("call connected and recording confirmed — proceed with call flow")
Important implementation details
• wait_for_participant_join / wait_for_participant_publishing_audio:
◦ Implement these by subscribing to participant lifecycle events or polling room state via the admin/room APIs.
◦ Check whether the participant has any published audio tracks and whether those tracks have nonzero audio levels for a short period (a few frames) to ensure the publisher actually started sending media.
• Webhook handling:
◦ Use the egress_started webhook to confirm the egress is actively recording. Treat the API start response as optimistic; rely on the webhook for proof-of-start.
◦ Use participant_joined and participant_published_track (or equivalent) events for both agent and caller.
• Timing and timeouts:
◦ Use conservative short timeouts (5–15s) to avoid hanging your call flow. If timeouts happen, abort dialing or retry egress based on your policy.
• Consider `track_composite_egress`:
◦ If timestamps still drift, track_composite_egress can let you pick exact tracks (agent + caller) and may produce cleaner sync between those tracks.
◦ Start track-based egress only once both tracks exist.
• Small startup buffer:
◦ If you observe very small residual offset, consider delaying the visible start of the final recording by a few hundred milliseconds after both tracks are active (trim the beginning or add a small aligned silence) to align perceived start-of-speech.
Next steps I can help with
• I can provide a runnable Python example tailored to your existing session.start() usage and webhook handlers (showing exact event names and parsing).
• Or, if you share the small snippets where you currently call session.start(...) and then dial, I will rewrite them to:
◦ start egress in a guarded way,
◦ wait for egress_started webhook,
◦ wait for both participants publishing audio,
◦ then proceed to dial or mark the recording as live.
Which would you prefer — a full runnable Python example using your current session.start() pattern, or I should sketch the exact webhook handlers and state machine to add to your entrypoint?