transfer_call tool; CXB Core resolves a compatible target for the active transport and executes the transport-specific path.
Transfer methods by transport
The source of truth is_TRANSPORT_METHODS in the Core service’s transfer-targets module:
| Transport | Method | Notes |
|---|---|---|
| WebSocket | websocket_reverse_transfer | Sends a reverse-call-transfer event over the WebSocket |
| LiveKit SIP (inbound) | livekit_sip_refer | SIP REFER via transfer_sip_participant |
| LiveKit SIP (outbound / attach) | livekit_sip_refer | SIP REFER via transfer_sip_participant |
| Exotel | — | Not supported. Deferred until Exotel exposes a dynamic call-control API |
Phone Transfer was dropped — it is no longer a transfer method. Only
websocket_reverse_transfer (WebSocket) and livekit_sip_refer (LiveKit SIP) exist as transport-executed methods. agent_desk is a separate escalation path, not a transport transfer method (see below).Transfer targets
Targets are configured per bot as a list ofTransferTarget objects (defined in the Core service’s config model):
| Field | Description |
|---|---|
id | Optional stable id (matched against a requested target) |
label | Human label (also matchable) |
method | websocket_reverse_transfer, livekit_sip_refer, or agent_desk |
type | extension, sip_uri, tel_uri, phone_number, or agent_queue |
value | Destination value |
is_default | Picked when no specific target is requested |
enabled | Disabled targets are ignored |
TransferTarget validates the method/type pairing — for example livekit_sip_refer accepts sip_uri / tel_uri / phone_number, sip_uri values must start with sip:, tel_uri values must start with tel:, and agent_desk only accepts agent_queue.
Resolution
resolve_transfer_target (in the transfer-targets module) picks a target:
Requested target
If the bot requested a target by id/label, return the matching compatible target (or a matching legacy fallback). No match raises
TransferTargetError.The legacy
transfer_numbers map (label → value) is still supported. For WebSocket it becomes an extension target; for LiveKit it is typed as sip_uri / tel_uri / phone_number based on the value prefix.X-header passthrough
SIP X-headers can carry context (e.g. CRM ids) to the transferred-to leg. Behavior diverges between inbound and outbound LiveKit SIP:| Path | Headers passed? |
|---|---|
| LiveKit SIP inbound | Yes — config.sip_context.transfer_headers are passed to transfer_sip_participant |
| LiveKit SIP outbound (dialout) | No — transfer_sip_participant called without headers |
| LiveKit SIP attach | No — transfer_sip_participant called without headers |
| WebSocket | Uses reverse-call-transfer (no SIP headers) |
reverse-call-transfer event (with streamId, callerId, did, transferno) and suppresses the serializer’s auto-hangup, since the transfer is terminal for the bot leg.
Agent Desk handoff
Agent Desk escalation is a separate path, not a value the transport executes. It is gated byagent_desk_enabled plus a target whose method is agent_desk. When both are present, the transfer_call tool routes to the Agent Desk callback instead of a SIP/WebSocket transfer (handled in the Core service’s pipeline factory).
- The handoff is enqueued first (via
enqueue_agent_desk_handoff_durable) so the bot only announces the transfer after the queue handoff succeeds. - A hold worker keeps the customer engaged while waiting for an agent.
- On a successful handoff,
disconnected_bybecomestransfer_to_agent(each LiveKit SIP route — inbound, outbound, and attach — sets this). This value is treated specially in post-call (the call was escalated, not abandoned).
Native transfer-state on call results
derive_transfer_state (in the Core service’s shared post-call logic) computes transfer fields from the pipeline event stream, for every transport (all transports emit the same transfer_sent / transfer_failed / tool_call events in the factory). These replaced the old phone_transfer field.
| Field | Meaning |
|---|---|
was_transferred | True only when a transfer_sent fired and no later transfer_failed invalidated it |
transfer_destination | Destination number/value from the transfer_sent event |
transfer_target | Target id/label from the matching transfer_call tool call |
transfer_reason | Reason from the transfer_sent event |
transfer_method | Method from the matching transfer_call tool call |
transfer_at | Timestamp of the transfer_sent event |
transfer_failed_reason | Error from a transfer_failed event (surfaced even for failed-only attempts) |
transfer_failed with no successful transfer_sent) still surface transfer_failed_reason, transfer_destination, and transfer_reason so ops can see them — but was_transferred stays false.
These fields are set on CallResults (the Core service’s results model) and surface to CRMs as result.* post-call webhook variables (e.g. result.was_transferred, result.transfer_destination).
Related
LiveKit SIP inbound
Inbound SIP transfer with X-header passthrough.
LiveKit SIP outbound
Outbound dialout transfer (no header passthrough).
WebSocket transport
Reverse-call-transfer over the WebSocket.
API contracts
result.* transfer-state fields in the results payload.