How to do call transfers with Five9
Configure LiveKit Agents to transfer calls back to Five9 IVR using X-Headers in SIP BYE messages.
Last Updated:
Five9 does not support SIP REFER for call transfers. Instead, Five9 uses a supervised external transfer model where routing details are passed via custom X-Headers in the SIP BYE message. LiveKit now supports this pattern, enabling seamless transfers back to Five9 IVR flows.
How it works
The integration follows this flow:
- Five9 IVR initiates the call: Preconfigured Call Variables (CAVs) in Five9 VCC are converted to custom X-Headers in the SIP INVITE.
- LiveKit receives the call: Your agent processes the call and extracts context from the incoming X-Headers.
- Agent sends SIP BYE with X-Headers: When the agent completes its task, it ends the SIP leg with a BYE message containing X-Headers for routing information (intents, entities, parameters).
- Five9 receives the headers: X-Headers in the BYE are converted back to CAVs, allowing the original IVR flow to continue routing based on the returned data.
Loading diagram…
Configuring the Five9 side
Setting outbound X-Headers
In your Five9 IVR, use the Set Variable module to configure Call Variables that will be converted to X-Headers:
| Five9 Function | Example Value |
|---|---|
PUT(ToIVA, "X-CallANI", Call.ANI) | Caller's phone number |
PUT(ToIVA, "X-CallDNIS", Call.DNIS) | Called number |
PUT(ToIVA, "X-CallID", Call.call_id) | Five9 call identifier |
PUT(ToIVA, "X-CallSessionID", Call.session_id) | Session identifier |
PUT(ToIVA, "X-CallCampaign", Call.campaign_name) | Campaign name |
PUT(ToIVA, "X-CallStartTimestamp", Call.start_timestamp) | Call start time |
PUT(ToIVA, "X-CallDomainID", Call.domain_id) | Domain identifier |
Receiving return X-Headers
Five9 automatically converts X-Headers from the SIP BYE into Call Variables:
| Five9 Function | Purpose |
|---|---|
TOSTRING(FromIVA) | Full response string |
GET(FromIVA, "X-RouteReason") | Why the call is being returned |
GET(FromIVA, "X-RouteType") | Type of routing action |
GET(FromIVA, "X-RouteValue") | Routing destination or value |
GET(FromIVA, "X-MetaData") | Additional context or JSON data |
Configuring the LiveKit inbound trunk
The key to this integration is the attributes_to_headers trunk configuration. This maps LiveKit participant attributes to SIP X-Headers that are automatically included when sending BYE messages.
Step 1: Create an inbound trunk with attribute-to-header mapping
Configure your inbound trunk to map participant attributes to X-Headers:
1{2"trunk": {3"name": "Five9 Inbound",4"numbers": ["+15551234567"],5"headers_to_attributes": {6"X-CallANI": "five9.call_ani",7"X-CallDNIS": "five9.call_dnis",8"X-CallID": "five9.call_id",9"X-CallSessionID": "five9.session_id"10},11"attributes_to_headers": {12"five9.route_reason": "X-RouteReason",13"five9.route_type": "X-RouteType",14"five9.route_value": "X-RouteValue",15"five9.metadata": "X-MetaData"16}17}18}
Create the trunk using the LiveKit CLI:
1lk sip inbound create five9-trunk.json
The attributes_to_headers mapping tells LiveKit: "When sending a SIP BYE, take the value of the five9.route_reason participant attribute and include it as the X-RouteReason header."
Configuring the LiveKit Agent
With the trunk configured, your agent sets participant attributes to define the routing values. When the call ends, LiveKit automatically includes these as X-Headers in the BYE message.
1from livekit import api2from livekit.agents import Agent, AgentSession, JobContext, get_job_context, function_tool, RunContext3from livekit.rtc import ParticipantKind45class Five9Agent(Agent):6def __init__(self):7super().__init__(8instructions="You are a helpful assistant. Determine the caller's intent and route them appropriately."9)1011@function_tool()12async def route_and_end_call(13self,14ctx: RunContext,15route_reason: str,16route_type: str,17route_value: str,18metadata: str = "",19) -> str:20"""Set routing info and end the call, returning to Five9 IVR."""2122job_ctx = get_job_context()2324# Find the SIP participant25sip_participant = None26for participant in job_ctx.room.remote_participants.values():27if participant.kind == ParticipantKind.PARTICIPANT_KIND_SIP:28sip_participant = participant29break3031if not sip_participant:32return "No SIP participant found."3334# Set participant attributes that will be mapped to X-Headers in BYE35await job_ctx.api.room.update_participant(36api.UpdateParticipantRequest(37room=job_ctx.room.name,38identity=sip_participant.identity,39attributes={40"five9.route_reason": route_reason,41"five9.route_type": route_type,42"five9.route_value": route_value,43"five9.metadata": metadata,44}45)46)4748# End the call - this sends BYE with X-Headers to Five949await job_ctx.api.room.delete_room(50api.DeleteRoomRequest(room=job_ctx.room.name)51)5253return f"Routing to {route_value} with reason: {route_reason}"
How it works
- Agent determines routing — Based on the conversation, your agent decides where to route the caller
- Set participant attributes — Use
update_participantto set attributes matching yourattributes_to_headersmapping - End the call — Deleting the room triggers a SIP BYE to Five9
- LiveKit includes X-Headers — The trunk config automatically maps attributes to X-Headers in the BYE
- Five9 receives routing info — Five9 converts X-Headers back to CAVs and continues the IVR flow
Example: Intent-based routing
A common pattern is to determine the caller's intent and route them to a specific skill or queue:
1from livekit import api2from livekit.agents import Agent, function_tool, RunContext, get_job_context3from livekit.rtc import ParticipantKind4import json56class IntentRoutingAgent(Agent):7def __init__(self):8super().__init__(9instructions="""You are a helpful IVR assistant. Determine the caller's intent10and route them to the appropriate department: sales, support, or billing."""11)1213@function_tool()14async def route_to_department(15self,16ctx: RunContext,17department: str,18reason: str,19) -> str:20"""Route the caller to a specific department in Five9."""2122job_ctx = get_job_context()2324# Find the SIP participant25sip_participant = None26for participant in job_ctx.room.remote_participants.values():27if participant.kind == ParticipantKind.PARTICIPANT_KIND_SIP:28sip_participant = participant29break3031if not sip_participant:32return "No SIP participant found."3334# Set routing attributes that will become X-Headers in BYE35await job_ctx.api.room.update_participant(36api.UpdateParticipantRequest(37room=job_ctx.room.name,38identity=sip_participant.identity,39attributes={40"five9.route_reason": reason,41"five9.route_type": "SKILL_TRANSFER",42"five9.route_value": department,43"five9.metadata": json.dumps({44"intent": "department_transfer",45"confidence": 0.95,46"entities": {"department": department}47}),48}49)50)5152# Say goodbye before ending53await ctx.session.generate_reply(54instructions=f"Tell the caller you're transferring them to {department}."55)5657# End the call - BYE with X-Headers goes to Five958await job_ctx.api.room.delete_room(59api.DeleteRoomRequest(room=job_ctx.room.name)60)6162return f"Routed to {department}"
Important notes
- Caller hangs up first: If the caller hangs up before the agent ends the call, Five9 will not receive the X-Headers. Your IVR flow should handle this case.
- Trunk configuration required: The
attributes_to_headersmapping must be configured on your inbound trunk for this to work. - Attribute naming: Use consistent attribute names between your trunk config and agent code.