A carrier is a SIP provider configuration plus the DID numbers it serves. CXB API stores carriers in the carriers MongoDB collection and mirrors them into LiveKit as SIP inbound/outbound trunks and a dispatch rule. Inbound calls map a dialed DID to a bot; outbound dialout selects a trunk by the from_number. The carrier and trunk-sync logic lives in the API service.

Endpoints

All routes are under /api/v1/carriers. Reads require admin; all writes require super_admin.
MethodPathRolePurpose
POST/super_adminCreate carrier + sync trunks
GET/adminList carriers
GET/{carrier_id}adminGet one carrier
PUT/{carrier_id}super_adminUpdate carrier + re-sync trunk auth/address
DELETE/{carrier_id}super_adminDelete carrier + clean up LiveKit resources
POST/{carrier_id}/didssuper_adminAdd a DID, re-sync trunk numbers
PUT/{carrier_id}/dids/{did_id}super_adminUpdate a DID (re-syncs numbers if direction changed)
DELETE/{carrier_id}/dids/{did_id}super_adminRemove a DID, re-sync trunk numbers
POST/{carrier_id}/syncsuper_adminForce re-sync against LiveKit
The LiveKit API client is built per-request from settings.livekit_url/api_key/api_secret. When livekit_url is unset the carrier is still stored, but its lk_sync_status stays pending. The force-sync endpoint returns 503 LiveKit not configured in that case.

Carrier model

Key fields from the carrier model:
FieldTypeNotes
name, sip_addressstrDisplay name and outbound SIP target
transportauto/tcp/udp/tlsOutbound trunk transport
destination_countrystrOutbound trunk hint
inbound_auth_modecredentials/ipip requires a non-empty inbound_allowed_addresses
inbound_auth_username/password, inbound_allowed_addressesper-direction overridesFall back to legacy auth_username/password/allowed_addresses
outbound_auth_username/passwordper-direction overridesFall back to legacy auth
outbound_cpsfloat (0.1–100)Carrier dial rate cap (used by CXB Dialler)
outbound_burstint (1–1000)Token-bucket burst
outbound_channelsint (1–500)Max simultaneous outbound channels
dids[]listEach DID has number, label, direction (both/inbound/outbound), bot_id
lk_inbound_trunk_id, lk_outbound_trunk_id, lk_dispatch_rule_idstr/nullLiveKit resource IDs
lk_sync_statuspending/synced/errorLast sync outcome
lk_sync_error, lk_last_synced_atstr/datetimeDiagnostics
All three password fields are masked as eight bullet characters (PASSWORD_MASK) in CarrierResponse. On PUT, update_carrier drops any password field whose value equals the mask so the real stored secret is not overwritten with bullets. The CXB Console carrier form must not re-send the masked render as a real value.

Trunk sync

_sync_create_trunks partitions DID numbers by direction and creates LiveKit resources:
  • Inbound numbers = DIDs with direction inbound or both; outbound numbers = outbound or both.
  • Inbound trunk auth/allowlist resolved by _resolve_inbound_auth (mode-aware); outbound by _resolve_outbound_auth.
  • The inbound trunk declares headers_to_attributes from DEFAULT_INBOUND_HEADERS_TO_ATTRIBUTES (e.g. X-Customer-Name, X-Lead-Id, X-Call-Ref, X-Campaign-Id), mapping each to the sip.h.<Header> attribute convention. LiveKit drops any X-header not listed here.
  • The dispatch rule uses SIPDispatchRuleIndividual(room_prefix="call-"), so inbound SIP calls land in call-* rooms — the prefix the LiveKit webhook handler keys on.
An outbound-only carrier (no inbound DIDs) intentionally gets no inbound trunk. An inbound trunk created with numbers=[] matches <any>, and LiveKit refuses a second <any> inbound trunk, which would collide across carriers and break all outbound trunks too. _sync_create_trunks skips inbound trunk + dispatch rule when there are no inbound DIDs; the outbound trunk falls back to numbers ["*"].

Updates and re-sync

  • update_carrier re-syncs trunk auth/address/transport via _sync_update_trunks. Inbound trunk replacement uses update_inbound_trunk_auth, which fetches the existing trunk and rebuilds it via the Replace API so headers_to_attributes survives (LiveKit’s field-update SDK cannot set it).
  • DID add/update/remove re-sync trunk numbers via _sync_did_numbers (and update_trunk_numbers, which silently no-ops if the LiveKit build does not support number updates).
  • sync_carrier distinguishes a genuinely partial state from a legitimately inbound-less carrier (inbound_complete/dispatch_complete are true when there are no inbound DIDs) so re-sync does not needlessly tear down a working outbound trunk. If an update reports a stale/not_found trunk, it deletes the stale IDs and recreates.

Trunk health loop

verify_and_repair_trunks lists live LiveKit trunk and dispatch-rule IDs, compares each carrier’s stored lk_* IDs against them, and calls sync_carrier for any carrier whose IDs are missing. Carriers in pending are skipped. It returns the count repaired. This runs as the periodic trunk health loop registered in app.py; a missing LiveKit connection makes it a no-op (returns 0).

Lookups for the call path

The SIP-header service and the SIP routes consume these helpers:
HelperUsed byReturns
lookup_did(number)inbound SIP dispatch{bot_id, carrier_id, did_id} for an inbound/both DID that has a bot_id
lookup_outbound_trunk(from_number)dialout{trunk_id, number, carrier_id, carrier_name, rate_limit} for an outbound/both DID (first available if from_number omitted)
rate_limit is derived by _carrier_rate_limit from outbound_cps/outbound_burst/outbound_channels and is consumed by CXB Dialler’s per-carrier rate limiter.

Inbound SIP dispatch

How a call-* room participant is routed to a fleet worker.

SIP dialout

Outbound trunk selection and dual-auth dialout.

LiveKit SIP inbound (CXB Core)

What CXB Core does after dispatch.

Fleet routing

How CXB API picks a fleet worker for each call.