Call Transfer
Call transfer allows your voice agent to seamlessly hand off calls to human agents or external numbers when needed.
Transfer Types
| Type | Description | Use Case |
|---|---|---|
| Warm Transfer | Agent stays on while connecting | Complex issues |
| Cold Transfer | Direct transfer, agent disconnects | Routine escalations |
| Conference | Add human while AI stays | Supervision |
Configuration
Agent Setup
{
"agent": {
"name": "Customer Support",
"transferEnabled": true,
"transferNumbers": {
"sales": "+14155551234",
"support": "+14155555678",
"billing": "+14155559012"
},
"transferMessage": "I'm connecting you with a specialist. Please hold."
}
}
Transfer Function
{
"tools": [
{
"type": "function",
"function": {
"name": "transfer_call",
"description": "Transfer the call to a human agent when you cannot help the customer or they request to speak with a human",
"parameters": {
"type": "object",
"properties": {
"department": {
"type": "string",
"enum": ["sales", "support", "billing"],
"description": "Department to transfer to"
},
"reason": {
"type": "string",
"description": "Brief reason for transfer"
}
},
"required": ["department"]
}
}
}
]
}
Implementation
Transfer Handler
type TransferHandler struct {
twilioClient *twilio.RestClient
plivoClient *plivo.Client
}
func (h *TransferHandler) TransferCall(ctx context.Context, params TransferParams) error {
// Play transfer message
if params.Message != "" {
h.pipeline.TTS.Speak(params.Message)
time.Sleep(2 * time.Second) // Wait for message to play
}
// Get transfer number
number := h.getTransferNumber(params.Department)
if number == "" {
return fmt.Errorf("unknown department: %s", params.Department)
}
// Execute transfer based on provider
switch h.provider {
case "twilio":
return h.transferTwilio(ctx, params.CallSID, number)
case "plivo":
return h.transferPlivo(ctx, params.CallUUID, number)
case "exotel":
return h.transferExotel(ctx, params.CallSID, number)
}
return nil
}
Twilio Transfer
func (h *TransferHandler) transferTwilio(ctx context.Context, callSid, number string) error {
// Create TwiML for transfer
twiml := fmt.Sprintf(`
<Response>
<Dial>
<Number>%s</Number>
</Dial>
</Response>
`, number)
// Update call with new TwiML
_, err := h.twilioClient.Api.UpdateCall(callSid, &api.UpdateCallParams{
Twiml: &twiml,
})
return err
}
// Alternative: Redirect to URL
func (h *TransferHandler) transferTwilioURL(ctx context.Context, callSid, department string) error {
url := fmt.Sprintf("https://your-server.com/api/twilio/transfer?dept=%s", department)
_, err := h.twilioClient.Api.UpdateCall(callSid, &api.UpdateCallParams{
Url: &url,
})
return err
}
Transfer Endpoint
func handleTransfer(w http.ResponseWriter, r *http.Request) {
department := r.URL.Query().Get("dept")
number := getTransferNumber(department)
response := twiml.VoiceResponse{}
response.Say("Please hold while I transfer you.")
response.Dial(twiml.Dial{
Number: number,
Timeout: 30,
CallerID: os.Getenv("TWILIO_PHONE_NUMBER"),
})
w.Header().Set("Content-Type", "text/xml")
response.WriteTo(w)
}
Warm Transfer
Stay on the call while connecting:
func (h *TransferHandler) warmTransfer(callSid, number string) error {
// Create conference
conferenceName := fmt.Sprintf("transfer-%s", uuid.New().String())
// Move current call to conference
twiml := fmt.Sprintf(`
<Response>
<Say>Please hold while I connect you.</Say>
<Dial>
<Conference
startConferenceOnEnter="true"
endConferenceOnExit="false">
%s
</Conference>
</Dial>
</Response>
`, conferenceName)
h.twilioClient.Api.UpdateCall(callSid, &api.UpdateCallParams{
Twiml: &twiml,
})
// Call the transfer number and add to conference
h.twilioClient.Api.CreateCall(&api.CreateCallParams{
To: number,
From: os.Getenv("TWILIO_PHONE_NUMBER"),
Twiml: fmt.Sprintf(`
<Response>
<Say>You have an incoming transfer.</Say>
<Dial>
<Conference
startConferenceOnEnter="true"
endConferenceOnExit="true">
%s
</Conference>
</Dial>
</Response>
`, conferenceName),
})
return nil
}
Transfer with Context
Pass conversation context to the human agent:
func (h *TransferHandler) transferWithContext(callSid, number string, context *ConversationContext) error {
// Generate summary for human agent
summary := h.generateSummary(context)
// Store for later retrieval
h.storeTransferContext(callSid, TransferContext{
CustomerPhone: context.CallerNumber,
CustomerName: context.CustomerName,
Summary: summary,
PreviousTopics: context.Topics,
Sentiment: context.Sentiment,
})
// Play whisper message to agent
twiml := fmt.Sprintf(`
<Response>
<Say>Transferring customer call.</Say>
<Dial>
<Number url="https://your-server.com/api/whisper/%s">
%s
</Number>
</Dial>
</Response>
`, callSid, number)
return h.twilioClient.Api.UpdateCall(callSid, &api.UpdateCallParams{
Twiml: &twiml,
})
}
// Whisper endpoint - only the agent hears this
func handleWhisper(w http.ResponseWriter, r *http.Request) {
callSid := chi.URLParam(r, "callSid")
context := getTransferContext(callSid)
response := twiml.VoiceResponse{}
response.Say(fmt.Sprintf(
"Incoming transfer. Customer: %s. Topic: %s. Sentiment: %s.",
context.CustomerName,
context.Summary,
context.Sentiment,
))
w.Header().Set("Content-Type", "text/xml")
response.WriteTo(w)
}
LLM Integration
Handle transfer requests naturally:
systemPrompt := `You are a customer support agent.
TRANSFER GUIDELINES:
- Transfer to "sales" for purchase inquiries
- Transfer to "support" for technical issues
- Transfer to "billing" for payment questions
When to transfer:
- Customer explicitly requests human agent
- Issue requires access you don't have
- Customer is frustrated after 2+ failed attempts
- Complex issues requiring investigation
Before transferring:
- Apologize for any inconvenience
- Explain you're connecting them to a specialist
- Provide brief context of what you've discussed`
// In function call handler
func handleFunctionCall(call ToolCall) (any, error) {
switch call.Name {
case "transfer_call":
var args struct {
Department string `json:"department"`
Reason string `json:"reason"`
}
json.Unmarshal(call.Arguments, &args)
return h.transferHandler.TransferCall(ctx, TransferParams{
CallSID: session.CallSID,
Department: args.Department,
Reason: args.Reason,
Message: "I'm connecting you with a specialist who can better assist you. Please hold.",
})
}
return nil, nil
}
Transfer Metrics
Track transfer patterns:
type TransferMetrics struct {
TotalTransfers int
TransfersByDept map[string]int
TransfersByReason map[string]int
AverageTimeToTransfer time.Duration
TransferSuccessRate float64
}
func (m *TransferMetrics) RecordTransfer(transfer TransferEvent) {
m.TotalTransfers++
m.TransfersByDept[transfer.Department]++
m.TransfersByReason[transfer.Reason]++
metrics.RecordCounter("transfers.total", 1)
metrics.RecordCounter(fmt.Sprintf("transfers.%s", transfer.Department), 1)
metrics.RecordHistogram("transfers.time_to_transfer_ms",
transfer.TimeToTransfer.Milliseconds())
}
Transfer Queue Integration
Connect to call center queue systems:
type QueueTransfer struct {
queueClient *CallCenterQueue
}
func (q *QueueTransfer) TransferToQueue(params TransferParams) error {
// Add to queue with priority based on sentiment
priority := q.calculatePriority(params.Context)
queueEntry := QueueEntry{
CallerID: params.CallerNumber,
CallSID: params.CallSID,
Department: params.Department,
Priority: priority,
Context: params.Context,
QueuedAt: time.Now(),
}
// Place in queue
position := q.queueClient.Enqueue(queueEntry)
// Play position update
message := fmt.Sprintf(
"You are number %d in the queue. Estimated wait time is %d minutes.",
position,
position * 2,
)
return q.playHoldMusic(params.CallSID, message)
}
func (q *QueueTransfer) calculatePriority(context *ConversationContext) int {
priority := 5 // Default
if context.Sentiment == "negative" {
priority = 2
}
if context.IsVIP {
priority = 1
}
if context.IssueType == "urgent" {
priority = 1
}
return priority
}
Error Handling
func (h *TransferHandler) TransferCall(ctx context.Context, params TransferParams) error {
// Try transfer
err := h.executeTransfer(ctx, params)
if err == nil {
return nil
}
// Log failure
log.Printf("Transfer failed: %v", err)
metrics.RecordCounter("transfers.failed", 1)
// Play error message to caller
h.pipeline.TTS.Speak(
"I'm sorry, I wasn't able to connect you right now. " +
"Would you like me to schedule a callback instead?",
)
// Offer callback as fallback
return h.offerCallback(ctx, params)
}
Best Practices
1. Always Confirm Before Transfer
systemPrompt := `Before transferring:
1. Confirm the user wants to be transferred
2. Briefly explain who they'll be speaking with
3. Ask if they need anything else first`
2. Handle Transfer Failures Gracefully
func (h *TransferHandler) handleFailure(params TransferParams) error {
// Record for follow-up
h.recordFailedTransfer(params)
// Offer alternatives
options := []string{
"Schedule a callback",
"Leave a voicemail",
"Send email to support",
}
return h.offerAlternatives(options)
}
3. Track Transfer Reasons
Analyze why transfers happen to improve the AI:
func analyzeTransferReasons() {
reasons := getTransferReasons(last30Days)
// If many transfers for same reason, improve AI handling
for reason, count := range reasons {
if count > threshold {
log.Printf("High transfer rate for: %s", reason)
// Alert to update training data
}
}
}
Next Steps
- Function Calling - Define transfer tool
- Webhooks - Post-transfer events
- Recording - Record transferred calls