alert-notebook-4408
03/25/2026, 7:58 AMtall-airline-16756
03/25/2026, 7:58 AMtall-airline-16756
03/25/2026, 7:58 AMCreateSIPParticipant 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 Documentationalert-notebook-4408
03/26/2026, 6:06 AMtall-airline-16756
03/26/2026, 6:07 AMproto_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):
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 elementsalert-notebook-4408
03/26/2026, 6:31 AMtall-airline-16756
03/26/2026, 6:31 AMsip_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):
# 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):
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 elementsalert-notebook-4408
03/26/2026, 6:59 AM@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 ?tall-airline-16756
03/26/2026, 7:00 AMparticipant_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)
# 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 elementsalert-notebook-4408
04/01/2026, 8:17 AM@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,
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 ?tall-airline-16756
04/01/2026, 8:17 AMparticipant_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:
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:
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
@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 elementsalert-notebook-4408
04/01/2026, 9:49 AMtall-airline-16756
04/01/2026, 9:50 AMTransferSIPParticipantRequest.
• 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