IVR Call Transfer Backend Reference
Overview
The IVR Call Transfer feature allows AI agents to transfer callers to external phone numbers. Two modes are supported:
- Cold Transfer SIP REFER redirect. Caller is forwarded directly, LiveKit session ends.
- Warm Transfer Agent dials the target, navigates IVR menus via DTMF tones, then bridges the original caller into the conversation.
Customers configure IVR actions (transfer targets, DTMF sequences, descriptions) through the API. Each action becomes a separate LLM tool at runtime.
Feature Gate
Feature string: "ivr_call_transfer" in AVAILABLE_FEATURES
Feature map: "manageIvrActions" → ["ivr_call_transfer"] in FEATURE_MAP
The requireFeatureAccess middleware enforces this gate on write endpoints. The feature must be present in both the Organization's plan.features and the Customer's features arrays.
API Endpoints
GET /api/agents/:agentId/ivr-actions
Returns the agent's configured IVR actions.
| Middleware | verifyJwt |
| Auth | JWT user must own the agent (agent.userId === req.userId) |
| Response | { success: true, ivrActions: IIvrAction[] } |
PUT /api/agents/:agentId/ivr-actions
Replaces the agent's entire ivrActions array. Sets is_agent_updated = true.
| Middleware | verifyJwt, requireRole("customer"), requireFeatureAccess("manageIvrActions") |
| Auth | JWT user must own the agent |
| Request body | { ivrActions: IIvrAction[] } |
| Response | { success: true, ivrActions: IIvrAction[] } |
GET /internal/agent-ivr-actions/:agentId
Internal endpoint for Agent Server inbound call fallback. Returns IVR actions without JWT auth.
| Middleware | verifyInternalWebhook (x-api-secret header) |
| Response | { success: true, ivrActions: IIvrAction[] } |
Data Model
IVR actions are stored as a sub-array on the Agent document (same pattern as tools, backchannels, templateVariables).
// Agent schema ivrActions field
ivrActions: [{
actionId: { type: String, required: true }, // Unique within the array
name: { type: String, required: true }, // Max 64 chars
description: { type: String, required: true }, // Max 512 chars, used as LLM tool description
transferTarget: { type: String, required: true }, // E.164 or SIP URI
transferType: { type: String, enum: ["cold", "warm"], default: "cold" },
dtmfSequence: [{
type: { type: String, enum: ["digit", "wait"], required: true },
value: { type: String, required: true }, // DTMF char or milliseconds
}],
}]TypeScript interface:
interface IIvrAction {
actionId: string;
name: string;
description: string;
transferTarget: string;
dtmfSequence?: Array<{ type: "digit" | "wait"; value: string }>;
transferType: "cold" | "warm";
}Validation Rules
| Field | Rule |
|---|---|
actionId | Required string, unique within the array |
name | Required, non-empty, max 64 characters |
description | Required, non-empty, max 512 characters |
transferTarget | Required, must match E.164 (/^\+[1-9]\d{1,14}$/) or SIP URI (/^sip:.+/) |
transferType | Required, "cold" or "warm" |
dtmfSequence[].type | "digit" or "wait" |
dtmfSequence[].value (digit) | One of 0-9, *, # |
dtmfSequence[].value (wait) | Integer between 100 and 30000 ms |
| Actions per agent | Maximum 20 |
Duplicate actionId | Rejected with validation error |
Validation is handled by validateIvrAction() in ivr-action.controller.ts. All fields are trimmed before persistence.
Dispatch Metadata Integration
IVR actions are embedded in the dispatch metadata payload sent to the Agent Server when creating a LiveKit room. This avoids extra API calls during live outbound/test calls.
// DispatchMetadata ivr_actions field
ivr_actions?: Array<{
actionId: string;
name: string;
description: string;
transferTarget: string;
dtmfSequence?: Array<{ type: "digit" | "wait"; value: string }>;
transferType: "cold" | "warm";
}>;The dispatch builder reads agent.ivrActions and includes it as ivr_actions in the metadata. For inbound calls where dispatch metadata may not contain IVR actions, the Agent Server falls back to the internal API endpoint.
Agent Server Integration
On the Agent Server (Python/LiveKit), IVR actions flow through:
- MetadataSchema Pydantic model parses
ivr_actionsfrom dispatch metadata intoIvrActionDefobjects. - Tool Registration
create_ivr_tool_functions()creates one LiveKitFunctionToolper action, using the action'snameanddescription. - Cold Transfer Tool calls
TransferSIPParticipant(SIP REFER). - Warm Transfer Tool dials the target via
create_sip_participant, executes the DTMF sequence step by step, then bridges the caller viaMoveParticipant. - IVR Detection When
ivr_actionsare present,session_builderpassesivr_detection=TruetoAgentSession. - Inbound Fallback If
ivr_actionsis absent from metadata, the Agent Server fetches fromGET /internal/agent-ivr-actions/:agentId.
Error Responses
| Scenario | Status | Response |
|---|---|---|
Customer lacks ivr_call_transfer feature | 403 | { success: false, message: "You don't have access to this feature..." } |
| Invalid field values (name, description, target) | 400 | { success: false, message: "<validation error>", errors: [...] } |
| Invalid DTMF digit | 400 | { success: false, message: "Invalid DTMF digit: must be 0-9, *, or #" } |
| Wait value out of range | 400 | { success: false, message: "Wait value must be between 100 and 30000 ms" } |
| More than 20 IVR actions | 400 | { success: false, message: "Maximum 20 IVR actions per agent" } |
| Duplicate actionId | 400 | { success: false, message: "Duplicate actionId: \"...\"" } |
ivrActions not an array | 400 | { success: false, message: "ivrActions must be an array" } |
| Agent not found | 404 | { success: false, message: "Agent not found" } |
| Agent belongs to different customer | 403 | { success: false, message: "Not authorized to modify this agent" } |
| Server error | 500 | { success: false, message: "Failed to update IVR actions" } |
File Map
controllers/ivr-action.controller.ts Validation + CRUD handlers (getIvrActions, updateIvrActions, getIvrActionsInternal)
routes/ivr-action.routes.ts Route definitions + middleware chains
models/Agent.ts ivrActions field on Agent schema
types/index.ts IIvrAction interface, AVAILABLE_FEATURES, FEATURE_MAP entriesRelationship to Call Flow Builder & AI Actions Panel
IVR actions are part of the unified Call Flow Builder system. The same data is presented in two UI views:
| UI View | Location | Presentation |
|---|---|---|
| AI Actions Panel | Agent Editor → Tools & Transfer → AI Actions tab | Toggle-based list with config forms |
| Call Flow Builder | Standalone page or Agent Editor → Call Flow Builder step | Visual canvas with draggable AI Transfer nodes |
Changes in either view are synced to the other via the shared callFlowStore (Zustand). When a user enables an AI action (e.g., "Transfer to Number") in the toggle panel, it automatically:
- Appears as a standalone AI Transfer node on the Call Flow Builder canvas
- Auto-enables the "Enable Call Flow" toggle for the relevant direction (inbound/outbound)
For the full system architecture, see docs/CALL-FLOW-BUILDER.md.
Use Cases & Examples
1. Cold Transfer Escalate to a person
Scenario: Customer calls AI agent and says "I want to speak to a manager."
- Agent says "Let me transfer you, please hold."
- SIP REFER forwards the call directly to the manager's number.
- Agent session ends. Caller is connected to the manager.
Configuration:
{
"actionId": "uuid-1",
"name": "transfer_to_manager",
"description": "Transfer the caller to a manager when they request human assistance",
"transferTarget": "+919876543210",
"transferType": "cold",
"dtmfSequence": []
}2. Cold Transfer Forward to a department
Scenario: "Transfer me to billing" → agent forwards to the billing team's direct line.
Same as above with a different target number. No IVR navigation needed because the target is a direct line.
3. Warm Transfer with DTMF Navigate an IVR menu
Scenario: Customer wants to be transferred to their bank's fraud department. The bank's number has an IVR: "Press 1 for English → Press 3 for fraud → Press # to confirm."
- Agent dials the bank's number, navigates the IVR via DTMF tones.
- Once the fraud department answers, agent bridges the caller in.
- Caller is now connected to the fraud department rep.
Configuration:
{
"actionId": "uuid-2",
"name": "transfer_to_bank_fraud",
"description": "Transfer the caller to the bank fraud department",
"transferTarget": "+911800123456",
"transferType": "warm",
"dtmfSequence": [
{ "type": "wait", "value": "3000" },
{ "type": "digit", "value": "1" },
{ "type": "wait", "value": "2000" },
{ "type": "digit", "value": "3" },
{ "type": "wait", "value": "1000" },
{ "type": "digit", "value": "#" }
]
}4. Warm Transfer without DTMF Connect to a supervisor
Scenario: AI handles a complaint call, customer is angry and wants a human. Supervisor's number is a direct mobile no IVR.
- Agent dials the supervisor into the caller's room.
- Supervisor answers, is now in the same room as the caller.
- Agent leaves. Caller and supervisor continue.
Configuration:
{
"actionId": "uuid-3",
"name": "transfer_to_supervisor",
"description": "Connect the caller to a supervisor for escalation",
"transferTarget": "+919988776655",
"transferType": "warm",
"dtmfSequence": []
}5. Multiple transfer targets per agent
Scenario: A real estate AI agent has three transfer options:
| Action | Target | Type | DTMF |
|---|---|---|---|
| Transfer to Sales | +91-9876543210 | cold | |
| Transfer to Site Visit Booking | +91-1800-XXX-XXXX | warm | 2 → wait → 1 |
| Transfer to Manager | +91-9988776655 | warm |
The LLM sees all three as separate tools and picks the right one based on conversation context. If the user says "I want to book a site visit", the LLM calls transfer_to_site_visit_booking.
6. Inbound call transfer
Scenario: Customer calls your inbound number, talks to the AI, then asks to speak to a human.
Works identically to outbound the agent finds the SIP participant in the room and initiates the transfer. Both cold and warm work for inbound calls.
Summary
| Use Case | Transfer Type | DTMF | When to use |
|---|---|---|---|
| Escalate to a person | Cold | No | Target is a direct line (mobile, desk phone) |
| Forward to department | Cold | No | Target is a department's direct number |
| Navigate target's IVR then connect | Warm | Yes | Target has an IVR menu to navigate |
| Connect to supervisor directly | Warm | No | Target is a direct line but you want agent to stay until connected |
| Multiple options per agent | Both | Optional | Agent needs different transfer destinations |
| Inbound caller wants transfer | Both | Optional | Same as outbound, works for inbound too |
Self-Hosted LiveKit Note
Warm transfer works on self-hosted LiveKit. The MoveParticipant API is Cloud-only, but our implementation uses a single-room approach instead: the supervisor is dialed directly into the caller's room, and the agent leaves. The end result is identical.