DEV Community

David Muñoz
David Muñoz

Posted on

Fixing the ElevenLabs Call Transfer Bug with Twilio

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 or end_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:

  1. Disable the native ElevenLabs tools for transfer and end call.

  2. Create custom tools in your backend:

  • transfer_call

  • end_call

  1. 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 and end_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)