CXB Core can hand a live call to a human via two transport-specific transfer methods, or escalate it to a queued human agent through Agent Desk. The bot triggers a transfer by calling the 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:
TransportMethodNotes
WebSocketwebsocket_reverse_transferSends a reverse-call-transfer event over the WebSocket
LiveKit SIP (inbound)livekit_sip_referSIP REFER via transfer_sip_participant
LiveKit SIP (outbound / attach)livekit_sip_referSIP REFER via transfer_sip_participant
ExotelNot 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 of TransferTarget objects (defined in the Core service’s config model):
FieldDescription
idOptional stable id (matched against a requested target)
labelHuman label (also matchable)
methodwebsocket_reverse_transfer, livekit_sip_refer, or agent_desk
typeextension, sip_uri, tel_uri, phone_number, or agent_queue
valueDestination value
is_defaultPicked when no specific target is requested
enabledDisabled 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:
1

Filter

Keep enabled targets whose method is compatible with the active transport.
2

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

Default

Otherwise return the is_default compatible target, else the first compatible target.
4

Legacy fallback

If no transfer_targets match, fall back to the legacy transfer_numbers dict, synthesizing a TransferTarget for the transport.
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:
PathHeaders passed?
LiveKit SIP inboundYesconfig.sip_context.transfer_headers are passed to transfer_sip_participant
LiveKit SIP outbound (dialout)Notransfer_sip_participant called without headers
LiveKit SIP attachNotransfer_sip_participant called without headers
WebSocketUses reverse-call-transfer (no SIP headers)
This divergence is intentional today but worth knowing: a transfer_headers config set on an outbound or attach-based call is not forwarded on the REFER. Only inbound LiveKit SIP transfers carry sip_context.transfer_headers.
The WebSocket path sends a 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 by agent_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_by becomes transfer_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.
FieldMeaning
was_transferredTrue only when a transfer_sent fired and no later transfer_failed invalidated it
transfer_destinationDestination number/value from the transfer_sent event
transfer_targetTarget id/label from the matching transfer_call tool call
transfer_reasonReason from the transfer_sent event
transfer_methodMethod from the matching transfer_call tool call
transfer_atTimestamp of the transfer_sent event
transfer_failed_reasonError from a transfer_failed event (surfaced even for failed-only attempts)
Failed-only attempts (a 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).

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.