Call Transfer

Transfer calls to human agents or external numbers when the voice agent cannot handle the request.

1 min read

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
Cold Transfer Direct transfer to a named destination; the agent disconnects Routine escalations
Fallback chain (Plivo) "Also ring" extra numbers, then try the next destination after a ring timeout Making sure someone answers
DTMF transfer menu Caller presses a key ("press 1 for sales") to pick the destination Simple routing

Warm (consult or whisper) transfer, conference transfer and SIP REFER are not available.

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)
}

Transfer with Context#

The human agent does not hear a whisper message. Instead, the call's transcript, summary and extracted fields are available in Call History and on the call.ended / call.analyzed webhooks, so your team can open the context while they talk to the caller.

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#

Was this page helpful?