Skip to main content
 
Field Guides

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:

  1. Five9 IVR initiates the call: Preconfigured Call Variables (CAVs) in Five9 VCC are converted to custom X-Headers in the SIP INVITE.
  2. LiveKit receives the call: Your agent processes the call and extracts context from the incoming X-Headers.
  3. 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).
  4. 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 FunctionExample 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 FunctionPurpose
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:

{
"trunk": {
"name": "Five9 Inbound",
"numbers": ["+15551234567"],
"headers_to_attributes": {
"X-CallANI": "five9.call_ani",
"X-CallDNIS": "five9.call_dnis",
"X-CallID": "five9.call_id",
"X-CallSessionID": "five9.session_id"
},
"attributes_to_headers": {
"five9.route_reason": "X-RouteReason",
"five9.route_type": "X-RouteType",
"five9.route_value": "X-RouteValue",
"five9.metadata": "X-MetaData"
}
}
}

Create the trunk using the LiveKit CLI:

lk 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.

from livekit import api
from livekit.agents import Agent, AgentSession, JobContext, get_job_context, function_tool, RunContext
from livekit.rtc import ParticipantKind
class Five9Agent(Agent):
def __init__(self):
super().__init__(
instructions="You are a helpful assistant. Determine the caller's intent and route them appropriately."
)
@function_tool()
async def route_and_end_call(
self,
ctx: RunContext,
route_reason: str,
route_type: str,
route_value: str,
metadata: str = "",
) -> str:
"""Set routing info and end the call, returning to Five9 IVR."""
job_ctx = get_job_context()
# Find the SIP participant
sip_participant = None
for participant in job_ctx.room.remote_participants.values():
if participant.kind == ParticipantKind.PARTICIPANT_KIND_SIP:
sip_participant = participant
break
if not sip_participant:
return "No SIP participant found."
# Set participant attributes that will be mapped to X-Headers in BYE
await job_ctx.api.room.update_participant(
api.UpdateParticipantRequest(
room=job_ctx.room.name,
identity=sip_participant.identity,
attributes={
"five9.route_reason": route_reason,
"five9.route_type": route_type,
"five9.route_value": route_value,
"five9.metadata": metadata,
}
)
)
# End the call - this sends BYE with X-Headers to Five9
await job_ctx.api.room.delete_room(
api.DeleteRoomRequest(room=job_ctx.room.name)
)
return f"Routing to {route_value} with reason: {route_reason}"

How it works

  1. Agent determines routing — Based on the conversation, your agent decides where to route the caller
  2. Set participant attributes — Use update_participant to set attributes matching your attributes_to_headers mapping
  3. End the call — Deleting the room triggers a SIP BYE to Five9
  4. LiveKit includes X-Headers — The trunk config automatically maps attributes to X-Headers in the BYE
  5. 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:

from livekit import api
from livekit.agents import Agent, function_tool, RunContext, get_job_context
from livekit.rtc import ParticipantKind
import json
class IntentRoutingAgent(Agent):
def __init__(self):
super().__init__(
instructions="""You are a helpful IVR assistant. Determine the caller's intent
and route them to the appropriate department: sales, support, or billing."""
)
@function_tool()
async def route_to_department(
self,
ctx: RunContext,
department: str,
reason: str,
) -> str:
"""Route the caller to a specific department in Five9."""
job_ctx = get_job_context()
# Find the SIP participant
sip_participant = None
for participant in job_ctx.room.remote_participants.values():
if participant.kind == ParticipantKind.PARTICIPANT_KIND_SIP:
sip_participant = participant
break
if not sip_participant:
return "No SIP participant found."
# Set routing attributes that will become X-Headers in BYE
await job_ctx.api.room.update_participant(
api.UpdateParticipantRequest(
room=job_ctx.room.name,
identity=sip_participant.identity,
attributes={
"five9.route_reason": reason,
"five9.route_type": "SKILL_TRANSFER",
"five9.route_value": department,
"five9.metadata": json.dumps({
"intent": "department_transfer",
"confidence": 0.95,
"entities": {"department": department}
}),
}
)
)
# Say goodbye before ending
await ctx.session.generate_reply(
instructions=f"Tell the caller you're transferring them to {department}."
)
# End the call - BYE with X-Headers goes to Five9
await job_ctx.api.room.delete_room(
api.DeleteRoomRequest(room=job_ctx.room.name)
)
return 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_headers mapping must be configured on your inbound trunk for this to work.
  • Attribute naming: Use consistent attribute names between your trunk config and agent code.