During a call i want to transfer the call to a hum...
# ask-ai
a
During a call i want to transfer the call to a human or another number, how do i do that, can i create another sip participant req ?
✅ 1
t
Thanks for reaching out, Ayush. I'm an AI assistant and I'm here to help. Give me a moment while I research this topic.
Thanks — I’m responding to your question about transferring an ongoing call to a human or another number, and whether creating another SIP participant request is the right approach. Short answer: don’t create a new
CreateSIPParticipant
request to transfer an active call. Use LiveKit’s SIP transfer API instead. What to use • Cold transfer (SIP REFER): Use the
TransferSIPParticipantRequest
(the
transfer_sip_participant()
API). This initiates a SIP REFER to transfer the existing call to the target number. • Warm transfer (agent-assisted): Use the
WarmTransferTask
if you need the agent to call the human first, pass context/conversation history, and then bridge or hand off the caller. WarmTransferTask creates a separate room, dials the agent, manages hold/music and merges calls as needed. Key implementation notes • Example usage (conceptual): a. Build a
TransferSIPParticipantRequest
with the participant identity, room name, and
transfer_to
number. b. Call
transfer_sip_participant()
to initiate the SIP REFER transfer. • To preserve the original caller ID when dialing the new number, set the
sip_number
field on the SIP participant where supported. Note some SIP providers may block caller-ID passthrough. If you tell me which transfer type you want (cold vs warm) and what runtime/language you’re using (server SDK, which language), I can give a concise code example or step‑by‑step for your environment. Sources: Company directory phone assistant | LiveKit Documentation | Preserving Caller ID During SIP Transfers in LiveKit Agents | Agents telephony integration | LiveKit Documentation | WarmTransferTask | LiveKit Documentation
a
i want to do a cold transfer, also what is meant by preserving the caller id ? i am using the pyton sdk, what is proto_sip ?
t
Thanks — I’ll walk you through cold transfers, what “preserving the caller ID” means, and what
proto_sip
is. I’ll also give a concise Python example for a cold transfer. Acknowledging your request • You want a cold transfer using the Python SDK. • You asked what preserving the caller ID means and what
proto_sip
refers to. Cold transfer (what to use) • For a cold transfer use the
TransferSIPParticipantRequest
and the
transfer_sip_participant()
API. This issues a SIP REFER to move the ongoing call directly to the target number. Preserving caller ID (what it means) • Preserving the caller ID means the destination (the human or number you’re transferring to) sees the original caller’s phone number as the caller, not the SIP gateway’s or AI system’s number. • To attempt this, set the
sip_number
field on the SIP participant when creating or transferring the call. Note: some SIP providers or carriers block caller-ID passthrough — in those cases the call may show your configured gateway number instead. What
proto_sip
is •
proto_sip
is the protobuf-generated Python module that contains the SIP-related request and response classes (for example
TransferSIPParticipantRequest
,
CreateSIPParticipantRequest
, etc.). You use those classes to build the request objects the SDK sends. Python example (cold transfer) 1. Build a
TransferSIPParticipantRequest
with the participant identity, room name, and the destination number in
transfer_to
. 2. Call
transfer_sip_participant()
on the LiveKit SIP API client. Example (conceptual Python):
Copy code
from livekit import LivekitService  # adjust to your actual SDK import
from proto_sip import TransferSIPParticipantRequest

# assume you have an authenticated livekit_api client (livekit_api.sip)
request = TransferSIPParticipantRequest(
    participant_identity="participant-123",
    room_name="room-abc",
    transfer_to="+15551231234",  # destination number to transfer to
    play_dialtone=True           # optional: play dialtone while transferring
)

await livekit_api.sip.transfer_sip_participant(request)
What I can help with next • If you want, I can: ◦ Provide a complete runnable snippet based on your exact Python SDK imports and authentication code. ◦ Show how to set the
sip_number
to preserve caller ID when creating or updating a SIP participant. ◦ Explain how to check whether your SIP provider allows caller-ID passthrough and what to do if they don’t. Which would you like me to show next? If you want the runnable snippet, tell me how you instantiate the LiveKit client in your app (sync vs async, SDK package name) and I’ll adapt the example. , with interactive elements
a
yeah show me how to preserve the sip_number
t
Thanks — I’ll show exactly how to preserve the caller ID (
sip_number
) and explain the limitation for true cold transfers. Quick summary of what I’m addressing • You want a cold transfer and want the destination to see the original caller’s number. • You’re using the Python SDK and asked how to set
sip_number
. What “preserve caller ID” means • Preserving caller ID means the receiving party sees the original caller’s phone number (the ANI) instead of the gateway/agent number. • Whether this succeeds depends on your SIP trunk/provider — some providers block or require verification for arbitrary caller IDs. If the trunk blocks passthrough, the displayed number will be the trunk’s default. Important limitation for REFER-based cold transfers • A true cold transfer using
TransferSIPParticipantRequest
(SIP REFER) hands off the existing call leg. That request itself doesn’t provide a
sip_number
field to override the From header. • If you need to guarantee a specific
From
for an outbound leg, you must place an outbound call (e.g., via
CreateSIPParticipantRequest
) and include
sip_number
. This is effectively creating a new call leg rather than a pure REFER, but is often used when you must control the outbound caller ID. • Even when you set
sip_number
in the SDK, the trunk/provider must allow ANI passthrough for the change to take effect. Python examples 1) If you are initiating an outbound leg (use this to set caller ID on the new leg):
Copy code
# Example: create an outbound SIP participant and set sip_number to preserve caller ID
from proto_sip import CreateSIPParticipantRequest

create_req = CreateSIPParticipantRequest(
    sip_trunk_id="your_sip_trunk_id",
    sip_call_to="+15551231234",          # destination number (agent)
    sip_number="+15559876543",           # original caller number to present (preserve ANI)
    room_name="room-abc",
    participant_identity="outbound-leg-1",
    play_dialtone=True,
    hide_phone_number=False,
    krisp_enabled=False,
    wait_until_answered=True,
)

# Use your livekit API client to send the request
await livekit_api.sip.create_sip_participant(create_req)
2) For a SIP REFER cold transfer (preferred when you just want to hand off existing call):
Copy code
from proto_sip import TransferSIPParticipantRequest

transfer_req = TransferSIPParticipantRequest(
    participant_identity="participant-123",
    room_name="room-abc",
    transfer_to="+15551231234",   # destination number for the REFER
    play_dialtone=True
)

await livekit_api.sip.transfer_sip_participant(transfer_req)
• Note:
TransferSIPParticipantRequest
does not let you set
sip_number
. Caller ID behavior during REFER depends on your SIP trunk and whether it preserves the original call’s From header. Checklist to ensure caller ID is preserved • Set
sip_number
to the original caller when creating outbound legs (
CreateSIPParticipantRequest
). • Confirm your SIP trunk/provider allows ANI/caller-ID passthrough or allows the number you want to present. • If provider enforces verified numbers only, ensure the
sip_number
is a verified/allowed number on that trunk. Would you like: • A full runnable snippet that includes your LiveKit client instantiation (sync vs async and your SDK import names)? • Help checking what your SIP provider supports or an example flow that dials the agent first (warm-like) to guarantee caller-ID control? Tell me which option and your SDK initialization style and I’ll tailor the snippet. Sources: Preserving Caller ID During SIP Transfers in LiveKit Agents, with interactive elements
a
wait so,
Copy code
@function_tool()
    async def transfer_call(self, ctx: RunContext):
        """
        This function is used to transfer the call to the transfer number.
        """

        await ctx.session.generate_reply("Please wait while we transfer the call to the transfer number")

        job_ctx = get_job_context()

        transfer_request = api.TransferSIPParticipantRequest(
            participant_identity=self.room.local_participant.identity,
            room_name=self.room.name,
            transfer_to=self.transfer_number,
        )
        await job_ctx.api.sip.transfer_sip_participant(transfer_request)
here is how i am implemtnting it, but the local participant will be my agent right, should i sent the participant identity as remote callers identity, how is it different ?
t
Thanks — good question. I’ll clarify which participant identity to use and why. Short answer • The
participant_identity
in
TransferSIPParticipantRequest
should be the identity of the participant you want to transfer (the call leg being handed off). For a typical agent transferring a caller to another number, that is the remote caller’s participant identity — not the local agent’s identity. Why it matters • The transfer API issues a SIP REFER for the specified participant’s call leg. If you pass the agent’s (local) identity, you would be attempting to transfer the agent’s leg instead of the caller’s leg — that won’t hand the caller off to the destination number. • Using the remote caller’s identity tells LiveKit “take this caller’s active call and REFER it to transfer_to.” How to get the right identity • When the call connects, you should have participant objects for both the local agent and the remote caller. Use the remote caller’s
identity
property (the participant identity created for that inbound SIP or client participant). • If you only have a reference to the local participant in your function, look up the room participants and find the caller (for example by role, metadata, or stored mapping you set when the inbound call arrived). Corrected example (conceptual)
Copy code
# find the remote caller's identity first (example lookup)
caller_identity = find_remote_caller_identity(self.room)  # implement lookup based on your app

transfer_request = api.TransferSIPParticipantRequest(
    participant_identity=caller_identity,      # remote caller's identity
    room_name=self.room.name,
    transfer_to=self.transfer_number,
)

await job_ctx.api.sip.transfer_sip_participant(transfer_request)
Practical tips • If your app tags participants with metadata (e.g., role="caller" or participant.metadata), use that to reliably locate the caller identity. • If there can be multiple remote participants, ensure you select the exact one representing the inbound SIP caller you intend to transfer. • After calling transfer, monitor the SIP responses/events so you can confirm success or handle failures (e.g., trunk rejects REFER or provider blocks caller ID passthrough). If you want, share how participants are created or how you currently identify the caller in your code (participant metadata, identities, or event handlers), and I’ll give a precise snippet showing how to find the caller identity and run the transfer in your setup. , with interactive elements
a
okay so in my code i have this currently
Copy code
@function_tool()
    async def transfer_call(self, ctx: RunContext):
        """
        This function is used to transfer the call to the transfer number.
        """

        await ctx.session.say("Please wait while we transfer the call to the transfer number")

        job_ctx = get_job_context()

        transfer_to = self.transfer_number if self.transfer_number.startswith(("sip:", "sips:", "tel:")) else f"tel:{self.transfer_number}"
        print(f"transferring call to {transfer_to}")
        transfer_request = api.TransferSIPParticipantRequest(
            participant_identity=self.participant.identity,
            room_name=self.room.name,
            transfer_to=transfer_to,
        )
        await job_ctx.api.sip.transfer_sip_participant(transfer_request)
But its giving error when executed,
Copy code
13:43:26.373 DEBUG… livekit.agents     executing tool  
                                         {"function": "transfer_call", "arguments": "{}", "speech_id": "speech_c489d8da5dc1", "pid": 33563, "job_id": 
"AJ_rPZzFCVh6dvt", "room_id": "RM_FASDWnvcb4Fm"}
transferring call to tel:+919403190785
    13:43:32.280 ERROR… livekit.agents     exception occurred while executing tool                                                                                    
                                           Traceback (most recent call last):                                                                                         
                                             File "/Users/ayushoswal/coding/inference-engine/.venv/lib/python3.11/site-packages/livekit/agents/voice/generation.py",  
                                           line 623, in _traceable_fnc_tool                                                                                           
                                               val = await function_callable()                                                                                        
                                                     ^^^^^^^^^^^^^^^^^^^^^^^^^                                                                                        
                                             File "/Users/ayushoswal/coding/inference-engine/voice_agent/base_agent_actions.py", line 182, in transfer_call           
                                               await job_ctx.api.sip.transfer_sip_participant(transfer_request)                                                       
                                             File "/Users/ayushoswal/coding/inference-engine/.venv/lib/python3.11/site-packages/livekit/api/sip_service.py", line     
                                           815, in transfer_sip_participant                                                                                           
                                               return await self._client.request(                                                                                     
                                                      ^^^^^^^^^^^^^^^^^^^^^^^^^^^                                                                                     
                                             File "/Users/ayushoswal/coding/inference-engine/.venv/lib/python3.11/site-packages/livekit/api/twirp_client.py", line    
                                           129, in request                                                                                                            
                                               raise TwirpError(                                                                                                      
                                           livekit.api.twirp_client.TwirpError: TwirpError(code=permission_denied, message=twirp error unknown: sip status: 403:      
                                           Forbidden, status=403, metadata={'error_details':                                                                          
                                           'CAcSGnNpcCBzdGF0dXM6IDQwMzogRm9yYmlkZGVuGjcKJXR5cGUuZ29vZ2xlYXBpcy5jb20vbGl2ZWtpdC5TSVBTdGF0dXMSDgiTAxIJRm9yYmlkZGVu',    
                                           'sip_status': 'Forbidden', 'sip_status_code': '403'})                                                                      
                                         {"function": "transfer_call", "speech_id": "speech_c489d8da5dc1", "pid": 33563, "job_id": "AJ_rPZzFCVh6dvt", "room_id": 
"RM_FASDWnvcb4Fm"}
Traceback (most recent call last):
  File "/Users/ayushoswal/coding/inference-engine/.venv/lib/python3.11/site-packages/livekit/agents/voice/generation.py", line 623, in _traceable_fnc_tool
    val = await function_callable()
          ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/ayushoswal/coding/inference-engine/voice_agent/base_agent_actions.py", line 182, in transfer_call
    await job_ctx.api.sip.transfer_sip_participant(transfer_request)
  File "/Users/ayushoswal/coding/inference-engine/.venv/lib/python3.11/site-packages/livekit/api/sip_service.py", line 815, in transfer_sip_participant
    return await self._client.request(
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/ayushoswal/coding/inference-engine/.venv/lib/python3.11/site-packages/livekit/api/twirp_client.py", line 129, in request
    raise TwirpError(
livekit.api.twirp_client.TwirpError: TwirpError(code=permission_denied, message=twirp error unknown: sip status: 403: Forbidden, status=403, 
metadata={'error_details': 'CAcSGnNpcCBzdGF0dXM6IDQwMzogRm9yYmlkZGVuGjcKJXR5cGUuZ29vZ2xlYXBpcy5jb20vbGl2ZWtpdC5TSVBTdGF0dXMSDgiTAxIJRm9yYmlkZGVu', 'sip_status': 
'Forbidden', 'sip_status_code': '403'})
how do i resolve this ?
t
Thanks — I’ll help you troubleshoot the 403 and confirm the right participant identity and transfer URI format. What I’m responding to • Your transfer code, the 403 Forbidden SIP error, and whether you should pass the agent (local) or remote caller identity. Immediate answers • Use the remote caller’s participant identity for
participant_identity
. Passing the local agent’s identity attempts to transfer the agent’s leg, not the caller’s. • The 403 SIP status means the SIP side rejected the REFER. Common causes: permission/scopes on the API token, your SIP trunk not permitting REFER or that destination URI format is not allowed. Concrete checks and fixes (in order) 1) Ensure you pass the caller’s participant identity • Find the remote caller participant in the room (by role, metadata, or stored mapping) and use that identity in the request. • Quick pattern: a. Inspect
self.room.participants
(or how participants are stored in your app). b. Identify the inbound caller (e.g., metadata role="caller" or by
participant.sid
you stored when call started). c. Use that identity:
Copy code
caller_identity = self.caller_participant.identity  # whichever your lookup returns
     transfer_request = api.TransferSIPParticipantRequest(
         participant_identity=caller_identity,
         room_name=self.room.name,
         transfer_to=transfer_to,
     )
2) Use a SIP URI if your trunk expects it • You already prefixed with
tel:
. Some trunks require
sip:
(or accept
tel:
). The 403 can happen if the trunk rejects the scheme. • Try using
sip:+1555...
for REFER targets, or confirm with your trunk which scheme it accepts. Example:
Copy code
transfer_to = self.transfer_number
  if not transfer_to.startswith(("sip:", "sips:")):
      transfer_to = f"sip:{transfer_to.lstrip('tel:')}"
3) Verify API token scopes / permissions • A 403 can mean the LiveKit/Twirp request was denied by the SIP gateway because the token used to call the API lacks permission. Confirm the token used to authenticate job_ctx.api has SIP transfer rights. • If you generate ephemeral tokens, ensure the grant includes SIP and transfer permissions. Share (or inspect) the token-generation snippet if you want help validating scopes. 4) Confirm SIP trunk supports REFER and caller-ID passthrough • Make sure your SIP trunk/provider allows server-side REFER transfers and does not block transfers to the destination number. • If the trunk blocks REFER, a common fallback is: ◦ Dial an outbound leg using
CreateSIPParticipantRequest
with
sip_number
set to the original caller to simulate a transfer (this creates a new outbound call and can present the original ANI if the trunk allows it). ◦ Then bridge/mute/leave the existing call as your flow requires. 5) Check SIP response details in the error • The Twirp error metadata included
sip_status: Forbidden
and a base64/encoded error_details — this confirms the SIP provider rejected the REFER. Inspect provider logs or ask for SIP logs from the trunk to learn why (e.g., destination not allowed, REFER not permitted, caller ID policy). Minimal corrected snippet to try
Copy code
@function_tool()
async def transfer_call(self, ctx: RunContext):
    await ctx.session.say("Please wait while we transfer the call to the transfer number")

    job_ctx = get_job_context()

    # Prefer sip: URI for REFER; adjust if your trunk expects tel:
    transfer_to = self.transfer_number
    if not transfer_to.startswith(("sip:", "sips:")):
        transfer_to = f"sip:{transfer_to.lstrip('tel:')}"

    # Use the remote caller identity, not the agent's identity
    caller_identity = self.caller_participant.identity  # implement lookup if needed

    transfer_request = api.TransferSIPParticipantRequest(
        participant_identity=caller_identity,
        room_name=self.room.name,
        transfer_to=transfer_to,
    )
    await job_ctx.api.sip.transfer_sip_participant(transfer_request)
If this still returns 403, please provide one of these so I can help further: • How you identify/store the inbound caller participant in code (so I can show exact lookup code). • The token-generation snippet you use to create the API client (so I can check scopes/claims included). • Whether your SIP trunk requires
sip:
vs
tel:
URIs and whether it supports REFER/transfers. I can then give the exact code edits to fix identity lookup, show how to add
sip_number
if we need to create an outbound leg instead, or suggest token changes to grant the right permissions. , with interactive elements
a
Do i need to configure anything specifically to support sip transfer, from twilio or livekit ?
t
Thanks — I’ll explain exactly what (if anything) you need to configure on Twilio and LiveKit to support SIP cold transfers, and give a short checklist so you can resolve the 403. Quick answer • LiveKit supports SIP cold transfers (SIP REFER) out of the box — you call
TransferSIPParticipantRequest
. • The SIP provider (Twilio in your case) must be configured to allow transfers and caller-ID passthrough. Most 403 Forbidden errors for REFER come from the trunk rejecting the transfer or caller-ID policy, not from LiveKit. What to check / configure 1) Twilio trunk settings (required) • Enable Call Transfer (SIP REFER) / PSTN transfer support on the Twilio trunk. • Set the trunk transfer-caller-id policy. If you want the destination to see the original caller’s number, configure the trunk to use the transferee’s From (Twilio option:
from-transferee
). • Ensure the trunk allows transfers to the destination scheme you send (
sip:
vs
tel:
) and does not restrict the destination number. • Example CLI change: twilio api trunking v1 trunks update --sid TRUNK_SID --transfer-mode enable-all --transfer-caller-id from-transferee 2) LiveKit side (what you must do) • Use
TransferSIPParticipantRequest
with the identity of the participant whose leg should be REFERed (the inbound caller), room name, and
transfer_to
URI. • No extra LiveKit configuration is required beyond using the proper API and correct participant identity. 3) URI scheme for transfer_to • Some trunks expect
sip:
URIs for REFER targets; others accept
tel:
. If you get a 403, try switching schemes (e.g.,
sip:+1555...
vs
tel:+1555...
). 4) Caller ID (preserving ANI) • Setting
sip_number
helps when you create a new outbound leg (
CreateSIPParticipantRequest
) so that the receiving party sees the original caller’s number. • For REFER-based cold transfers, preservation of caller ID is determined by the trunk/provider and the trunk’s caller-ID policy (Twilio’s
from-transferee
or similar). If the trunk blocks passthrough, the caller ID will be rewritten or the REFER may be rejected. 5) Authentication / permissions • A 403 can mean LiveKit’s SIP gateway rejected the REFER because of: ◦ Token lacking required scopes/claims for SIP operations. ◦ Provider-side policy blocking REFER for that account or number. • If you create ephemeral tokens, make sure the token includes the permissions you expect for SIP actions. Troubleshooting steps (in order) 1. Confirm you are passing the inbound caller’s participant identity to
TransferSIPParticipantRequest
(not the agent/local identity). 2. Try switching
transfer_to
scheme: if you send
tel:...
, try
sip:...
instead. 3. Verify Twilio trunk settings: enable REFER/PSTN transfer and set the caller-id policy to
from-transferee
if you want original ANI preserved. 4. Check trunk/account logs on Twilio for the rejected REFER — Twilio will usually give a reason in SIP logs. 5. Verify your token or API client has the necessary permissions for SIP operations. Share how you generate the token if you want me to check it. 6. If REFER is blocked by the trunk, fallback option: create a new outbound leg with
CreateSIPParticipantRequest
and set
sip_number
to the original caller number, then bridge/leave as needed. Minimal checklist for your current 403 • Use remote caller identity in
participant_identity
. • Try
sip:
scheme for
transfer_to
. • Confirm Twilio trunk has REFER enabled and caller-id/pass-through set (e.g.,
from-transferee
). • Inspect Twilio SIP logs for the exact rejection reason. • Confirm token/permissions. If you want, I can: • Show the exact Twilio trunk CLI command you should run (I can tailor it to your trunk SID). • Inspect your token-generation snippet (paste it minus secrets) and confirm required scopes/claims. • Provide a short code snippet that finds the remote caller participant identity in your room and issues the transfer with a
sip:
URI. Which of those would you like me to do next? Sources: Agents telephony integration | LiveKit Documentation | WarmTransferTask | LiveKit Documentation | Preserving Caller ID During SIP Transfers in LiveKit Agents | Handling payments with PCI compliance | LiveKit | Preserve caller ID during SIP transfers with Twilio | LiveKit, with interactive elements