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:

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:

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

1
from livekit import api
2
from livekit.agents import Agent, AgentSession, JobContext, get_job_context, function_tool, RunContext
3
from livekit.rtc import ParticipantKind
4
5
class Five9Agent(Agent):
6
def __init__(self):
7
super().__init__(
8
instructions="You are a helpful assistant. Determine the caller's intent and route them appropriately."
9
)
10
11
@function_tool()
12
async def route_and_end_call(
13
self,
14
ctx: RunContext,
15
route_reason: str,
16
route_type: str,
17
route_value: str,
18
metadata: str = "",
19
) -> str:
20
"""Set routing info and end the call, returning to Five9 IVR."""
21
22
job_ctx = get_job_context()
23
24
# Find the SIP participant
25
sip_participant = None
26
for participant in job_ctx.room.remote_participants.values():
27
if participant.kind == ParticipantKind.PARTICIPANT_KIND_SIP:
28
sip_participant = participant
29
break
30
31
if not sip_participant:
32
return "No SIP participant found."
33
34
# Set participant attributes that will be mapped to X-Headers in BYE
35
await job_ctx.api.room.update_participant(
36
api.UpdateParticipantRequest(
37
room=job_ctx.room.name,
38
identity=sip_participant.identity,
39
attributes={
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
)
47
48
# End the call - this sends BYE with X-Headers to Five9
49
await job_ctx.api.room.delete_room(
50
api.DeleteRoomRequest(room=job_ctx.room.name)
51
)
52
53
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:

1
from livekit import api
2
from livekit.agents import Agent, function_tool, RunContext, get_job_context
3
from livekit.rtc import ParticipantKind
4
import json
5
6
class IntentRoutingAgent(Agent):
7
def __init__(self):
8
super().__init__(
9
instructions="""You are a helpful IVR assistant. Determine the caller's intent
10
and route them to the appropriate department: sales, support, or billing."""
11
)
12
13
@function_tool()
14
async def route_to_department(
15
self,
16
ctx: RunContext,
17
department: str,
18
reason: str,
19
) -> str:
20
"""Route the caller to a specific department in Five9."""
21
22
job_ctx = get_job_context()
23
24
# Find the SIP participant
25
sip_participant = None
26
for participant in job_ctx.room.remote_participants.values():
27
if participant.kind == ParticipantKind.PARTICIPANT_KIND_SIP:
28
sip_participant = participant
29
break
30
31
if not sip_participant:
32
return "No SIP participant found."
33
34
# Set routing attributes that will become X-Headers in BYE
35
await job_ctx.api.room.update_participant(
36
api.UpdateParticipantRequest(
37
room=job_ctx.room.name,
38
identity=sip_participant.identity,
39
attributes={
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
)
51
52
# Say goodbye before ending
53
await ctx.session.generate_reply(
54
instructions=f"Tell the caller you're transferring them to {department}."
55
)
56
57
# End the call - BYE with X-Headers goes to Five9
58
await job_ctx.api.room.delete_room(
59
api.DeleteRoomRequest(room=job_ctx.room.name)
60
)
61
62
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.