A note on frustration
For more than three months, I’ve been reporting this bug to ElevenLabs, along with several other developers who have asked for a fix. Unfortunately, it hasn’t been resolved, and the only practical way forward is to apply this kind of workaround.
It’s frustrating to patch around such a core issue, but until it’s fixed, this manual solution is the only way to deliver a smooth caller experience.
Special thanks to Simon, who pointed me in the right direction and helped unlock the proper approach to solving this cutoff bug. 🙏
Context
When building voice agents with ElevenLabs integrated with Twilio (either through a SIP trunk or via native Twilio integration), you often need the agent to transfer a call or end a call gracefully.
The problem: there’s a bug in ElevenLabs.
When the agent executes a call transfer, the call is cut off immediately, even before the agent finishes speaking.
The same happens when the agent ends a call—the cutoff is abrupt and unnatural.
This ruins the experience: the agent might say “Please hold while I transfer you…” but the line goes dead in the middle of the sentence.
Why this happens
Internally, ElevenLabs doesn’t handle the timing of call control well:
When
transfer
orend_call
is triggered, the audio stream is cut right away.There’s no native way to delay the cutoff until after the agent finishes speaking.
And this issue occurs in both cases:
SIP trunk integration with ElevenLabs
Native Twilio integration with ElevenLabs
The solution: implement transfer_call
and end_call
manually
⚠️ Important limitation:
This fix only works for numbers added through Twilio’s native integration (Programmable Voice).
If your numbers are connected via a SIP Trunk, Twilio does not allow updating or redirecting calls through the API. That’s why you’ll still get a 404 error when trying to apply this workaround on SIP Trunk calls.
(Reference: Twilio docs — Programmable Voice calls can be updated via API, SIP Trunk calls cannot.)
To fix this, you need to take control away from ElevenLabs and handle the call lifecycle yourself.
The approach:
Disable the native ElevenLabs tools for transfer and end call.
Create custom tools in your backend:
transfer_call
end_call
- When ElevenLabs invokes one of these tools:
Immediately return
200 OK
so ElevenLabs thinks everything worked and keeps the audio stream alive.Enqueue a background job with a short delay (2–5 seconds).
That job uses the Twilio REST API to perform the real action:
Update the call with a new TwiML
<Dial>
for transfers.Or mark the call as
completed
to hang up gracefully.
This way, the agent can finish the spoken message before the transfer or termination actually happens.
Example: transfer_call
in Ruby on Rails
Job to trigger the transfer
class PhoneAi::TransferCallJob < ActiveJob::Base
queue_as :default
def perform(call_sid, destination)
client = Twilio::REST::Client.new
Rails.logger.info "📞 [TransferCallJob] call_sid=#{call_sid}, destination=#{destination}"
result = client.calls(call_sid).update(
url: Rails.application.routes.url_helpers.transfer_twiml_voice_calls_url(
destination: destination,
host: "my.host.com" # Make sure you have declared your route accordingly.
),
method: "POST"
)
Rails.logger.info "✅ [TransferCallJob] Transfer success: SID=#{result.sid}, Status=#{result.status}"
rescue Twilio::REST::RestError => e
Rails.logger.error "❌ [TransferCallJob] Twilio error: #{e.message} (code=#{e.code})"
raise
end
end
TwiML endpoint for transfers
class Voice::CallsController < ApplicationController
def transfer_twiml
destination = params[:destination]
response = Twilio::TwiML::VoiceResponse.new do |r|
r.dial(number: destination)
end
render xml: response.to_s
end
end
Example: end_call
in Ruby on Rails
Job to end the call
class PhoneAi::EndCallJob < ActiveJob::Base
queue_as :default
def perform(call_sid)
client = Twilio::REST::Client.new
Rails.logger.info "📞 [EndCallJob] Ending call - call_sid=#{call_sid}"
# You must specify in your system prompt to use this tool after saying goodbye; or you can use the job and delay strategy again so it does not get cutoff.
result = client.calls(call_sid).update(status: "completed")
Rails.logger.info "✅ [EndCallJob] Call ended: SID=#{result.sid}, Status=#{result.status}"
rescue Twilio::REST::RestError => e
Rails.logger.error "❌ [EndCallJob] Twilio error: #{e.message} (code=#{e.code})"
raise
end
end
How to use
ElevenLabs announces: “Thank you for calling, goodbye.”
It triggers
end_call
.Your Rails backend responds immediately with
200 OK
.2 seconds later, the job executes
update(status: "completed")
and the call ends naturally.
Benefits of this approach
✔️ The agent finishes speaking before the line is cut.
✔️ You control the delay precisely (2s, 5s, etc).
✔️ Works for both transfers and call termination.
Conclusion
If you’re integrating ElevenLabs Voice AI with Twilio, you’ll likely encounter this bug: transfers and call endings are cut off too early.
The fix is to:
Implement your own tools (
transfer_call
andend_call
).Delegate the actual logic to Twilio using its REST API.
Insert a small delay so the agent can finish speaking before the action executes.
This gives you full control of the call flow and dramatically improves the caller’s experience.
Top comments (0)