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

POST /livekit/dialout
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"
}
FieldTypeRequiredDescription
bot_idstringYesBot configuration to use
to_numberstringYesNumber to dial
from_numberstringNoCaller ID. If omitted, CXB API selects a trunk.
connected_eventobjectNoArbitrary metadata forwarded to CXB API during config fetch
config_urlstringNoMulti-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:
InputResult
+91XXXXXXXXXXpassed as-is
91XXXXXXXXXX+ prefix added
0XXXXXXXXXXpassed as-is (local/STD format)

Response

{
  "status": "accepted",
  "call_id": "uuid-string",
  "room_name": "dialout-uuid-string"
}

Audio format

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:
  1. 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.
  2. CXB Core waits ~2s for the pipeline to start, then fast-fails the call as error if the pipeline died during startup.
  3. 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.
  4. 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 codeMeaningdisconnected_by
408Request timeoutno_answer
480Temporarily unavailableno_answer
486Busy hererejected
603Declinerejected
OtherUnexpected errorerror

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