The LiveKit SIP outbound transport handles outbound calls initiated by CXB API. CXB Core creates a LiveKit room, dials out via a SIP trunk, and runs the pipeline when the callee answers.
Connection flow
Endpoint
Requires X-CXB-Core-Secret header for authentication.
Request body
{
"bot_id": "bot-uuid",
"to_number": "+919876543210",
"from_number": "+911234567890",
"connected_event": {"campaign_id": "abc"},
"config_url": "https://api.cxbridge.io/api/v1/config"
}
| Field | Type | Required | Description |
|---|
bot_id | string | Yes | Bot configuration to use |
to_number | string | Yes | Number to dial |
from_number | string | No | Caller ID. If omitted, CXB API selects a trunk. |
connected_event | object | No | Arbitrary metadata forwarded to CXB API during config fetch |
config_url | string | No | Multi-client passthrough. When set, overrides the CXB_API_CONFIG_URL env var for both the config fetch and the outbound trunk lookup (the trunk base URL is derived from it). Lets one shared fleet serve multiple CXB API instances. |
Phone normalization
to_number is normalized by _normalize_phone() before dialing, accepting common formats:
| Input | Result |
|---|
+91XXXXXXXXXX | passed as-is |
91XXXXXXXXXX | + prefix added |
0XXXXXXXXXX | passed as-is (local/STD format) |
Response
{
"status": "accepted",
"call_id": "uuid-string",
"room_name": "dialout-uuid-string"
}
Same as LiveKit SIP inbound: 16kHz mono LINEAR16 via LiveKit SDK.
Dial-out behavior
CXB Core calls lkapi.sip.create_sip_participant() with a 45-second ringing timeout and wait_until_answered=True.
Startup ordering
The pipeline starts first so the bot is fully joined and ready when the callee answers — but the opening message is held back until the call is actually answered:
- The pipeline task is created with a
call_answered event (initially unset). The factory’s connect handler awaits this event before queueing the opening message.
- CXB Core waits ~2s for the pipeline to start, then fast-fails the call as
error if the pipeline died during startup.
create_sip_participant(wait_until_answered=True) dials out. on_first_participant_joined fires when the SIP leg enters the room during ringing — before pickup.
- Only after
create_sip_participant() returns (callee answered) does CXB Core call call_answered.set(), releasing the factory to speak the opening message.
This prevents the bot talking over the ring-back tone.
SIP failure codes
| SIP code | Meaning | disconnected_by |
|---|
| 408 | Request timeout | no_answer |
| 480 | Temporarily unavailable | no_answer |
| 486 | Busy here | rejected |
| 603 | Decline | rejected |
| Other | Unexpected error | error |
Trunk selection
CXB Core resolves the outbound SIP trunk by calling CXB API:
GET {cxb_api_url}/sip/outbound-trunk?from_number={from_number}
Returns {"trunk_id": "...", "number": "..."}. If from_number is omitted, CXB API selects an available trunk. The trunk lookup base URL is derived from config_url when provided, otherwise from CXB_API_CONFIG_URL.
Transfer
CXB Core transfers an outbound call by issuing a SIP REFER through lkapi.sip.transfer_sip_participant() (transfer method livekit_sip_refer), targeting the participant identity sip-{to_number}.
Unlike inbound, the outbound transfer does not pass transfer_headers on the REFER — it sends no headers argument. This is a deliberate divergence from /livekit/dispatch, which forwards config.sip_context.transfer_headers.
Agent Desk handoff
Outbound calls support the same human-agent escalation as inbound. When the bot escalates via the transfer_call tool / agent_desk_callback, CXB Core enqueues a durable handoff to CXB API and starts a hold worker instead of doing a SIP REFER. A successful enqueue sets disconnected_by = "transfer_to_agent", and the hard route timeout does not tear down a handed-off call.
Prerequisites
- LiveKit server with outbound SIP trunks configured
- Outbound trunk registered in CXB API
- Environment variables:
LIVEKIT_URL, LIVEKIT_API_KEY, LIVEKIT_API_SECRET